@roll-agent/core 0.18.0 → 0.20.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.
Files changed (99) hide show
  1. package/THIRD_PARTY_NOTICES.txt +112 -0
  2. package/dist/cli/chat/banner.d.ts +13 -0
  3. package/dist/cli/chat/banner.js +1 -1
  4. package/dist/cli/chat/ink/app.d.ts +4 -1
  5. package/dist/cli/chat/ink/app.js +1 -1
  6. package/dist/cli/chat/ink/banner-view.d.ts +15 -0
  7. package/dist/cli/chat/ink/banner-view.js +1 -0
  8. package/dist/cli/chat/ink/commands.d.ts +4 -12
  9. package/dist/cli/chat/ink/commands.js +1 -1
  10. package/dist/cli/chat/ink/history-from-messages.js +1 -1
  11. package/dist/cli/chat/ink/history-item.js +1 -1
  12. package/dist/cli/chat/ink/input-history.d.ts +2 -0
  13. package/dist/cli/chat/ink/input-history.js +1 -0
  14. package/dist/cli/chat/ink/line-buffer.d.ts +2 -0
  15. package/dist/cli/chat/ink/line-buffer.js +1 -1
  16. package/dist/cli/chat/ink/live-region.d.ts +2 -0
  17. package/dist/cli/chat/ink/live-region.js +1 -1
  18. package/dist/cli/chat/ink/reasoning-block.d.ts +4 -0
  19. package/dist/cli/chat/ink/reasoning-block.js +1 -0
  20. package/dist/cli/chat/ink/run-ink-repl.js +1 -1
  21. package/dist/cli/chat/ink/spinner.d.ts +3 -1
  22. package/dist/cli/chat/ink/spinner.js +1 -1
  23. package/dist/cli/chat/ink/state.d.ts +26 -3
  24. package/dist/cli/chat/ink/state.js +1 -1
  25. package/dist/cli/chat/ink/text-prompt.d.ts +3 -1
  26. package/dist/cli/chat/ink/text-prompt.js +1 -1
  27. package/dist/cli/chat/ink/turn-activity.d.ts +19 -0
  28. package/dist/cli/chat/ink/turn-activity.js +1 -0
  29. package/dist/cli/chat/ink/turn-status-line.d.ts +14 -0
  30. package/dist/cli/chat/ink/turn-status-line.js +1 -0
  31. package/dist/cli/chat/ink/use-session.js +1 -1
  32. package/dist/cli/commands/chat.d.ts +28 -0
  33. package/dist/cli/commands/chat.js +1 -1
  34. package/dist/cli/commands/config-explain.js +1 -1
  35. package/dist/cli/commands/config-setup.js +1 -1
  36. package/dist/cli/commands/config.js +1 -1
  37. package/dist/cli/commands/doctor.js +1 -1
  38. package/dist/cli/commands/ui.d.ts +30 -0
  39. package/dist/cli/commands/ui.js +1 -0
  40. package/dist/cli/index.js +1 -1
  41. package/dist/cli/utils/chat-renderer.js +1 -1
  42. package/dist/cli/utils/current-cli-shim.d.ts +14 -0
  43. package/dist/cli/utils/current-cli-shim.js +1 -0
  44. package/dist/cli/utils/debug-format.d.ts +4 -0
  45. package/dist/cli/utils/debug-format.js +1 -0
  46. package/dist/config/application-service.d.ts +90 -0
  47. package/dist/config/application-service.js +1 -0
  48. package/dist/config/catalog.d.ts +76 -0
  49. package/dist/config/catalog.js +1 -0
  50. package/dist/config/defaults.d.ts +4 -3
  51. package/dist/config/defaults.js +1 -1
  52. package/dist/config/document-store.d.ts +70 -0
  53. package/dist/config/document-store.js +1 -0
  54. package/dist/config/guidance.d.ts +571 -0
  55. package/dist/config/guidance.js +1 -0
  56. package/dist/config/index.d.ts +6 -0
  57. package/dist/config/index.js +1 -1
  58. package/dist/config/key-codec.d.ts +24 -3
  59. package/dist/config/key-codec.js +1 -1
  60. package/dist/config/migration.js +1 -1
  61. package/dist/config/schema.d.ts +52 -7
  62. package/dist/config/schema.js +1 -1
  63. package/dist/config/secret-policy.d.ts +20 -0
  64. package/dist/config/secret-policy.js +1 -0
  65. package/dist/llm/providers.d.ts +8 -4
  66. package/dist/llm/providers.js +1 -1
  67. package/dist/registry/agent-lifecycle.d.ts +97 -0
  68. package/dist/registry/agent-lifecycle.js +1 -0
  69. package/dist/registry/discovery.js +1 -1
  70. package/dist/registry/install.js +1 -1
  71. package/dist/registry/process-identity.d.ts +14 -0
  72. package/dist/registry/process-identity.js +1 -0
  73. package/dist/registry/process-manager.d.ts +28 -7
  74. package/dist/registry/process-manager.js +1 -1
  75. package/dist/registry/store.js +1 -1
  76. package/dist/skills/invocation.d.ts +27 -0
  77. package/dist/skills/invocation.js +1 -0
  78. package/dist/types/agent.d.ts +6 -0
  79. package/dist/types/agent.js +1 -1
  80. package/dist/types/chat.d.ts +3 -0
  81. package/dist/ui/config-controller.d.ts +15 -0
  82. package/dist/ui/config-controller.js +1 -0
  83. package/dist/ui/contracts.d.ts +49 -0
  84. package/dist/ui/contracts.js +1 -0
  85. package/dist/ui/index.d.ts +8 -0
  86. package/dist/ui/index.js +1 -0
  87. package/dist/ui/runtime-controller.d.ts +38 -0
  88. package/dist/ui/runtime-controller.js +1 -0
  89. package/dist/ui/server.d.ts +21 -0
  90. package/dist/ui/server.js +1 -0
  91. package/dist/ui/static-assets.d.ts +2 -0
  92. package/dist/ui/static-assets.js +1 -0
  93. package/dist/ui-assets/assets/app.css +1 -0
  94. package/dist/ui-assets/assets/app.js +53 -0
  95. package/dist/ui-assets/assets/roll-mark-wXIkY5y0.svg +5 -0
  96. package/dist/ui-assets/index.html +17 -0
  97. package/package.json +16 -8
  98. package/dist/cli/commands/config-guidance.d.ts +0 -148
  99. package/dist/cli/commands/config-guidance.js +0 -1
@@ -0,0 +1,97 @@
1
+ import type { ConfigActivationEffect } from "../config/application-service.ts";
2
+ import type { RollConfig } from "../config/schema.ts";
3
+ import type { AgentRuntimeOwnership, AgentStatus, RegisteredAgent } from "../types/agent.ts";
4
+ import { type AgentLifecycleLock, type ManagedAgentRuntimeIdentity, type ManagedAgentRuntimeInspection } from "./process-manager.ts";
5
+ export declare const AGENT_LIFECYCLE_STATES: readonly ["ready-on-demand", "running", "stopped", "unreachable", "external-online", "external-unreachable"];
6
+ export type AgentLifecycleState = (typeof AGENT_LIFECYCLE_STATES)[number];
7
+ export declare const AGENT_ACTIVATION_STATUSES: readonly ["deferred", "restarted", "kept-stopped", "next-invocation", "manual", "runtime-changed", "failed"];
8
+ export type AgentActivationStatus = (typeof AGENT_ACTIVATION_STATUSES)[number];
9
+ export interface BrowserRuntimeInspectionBoundary {
10
+ readonly state: "not-inspected";
11
+ readonly message: string;
12
+ }
13
+ export interface AgentLifecycleInspection {
14
+ readonly agentName: string;
15
+ readonly ownership: AgentRuntimeOwnership;
16
+ readonly transport: RegisteredAgent["transport"]["type"];
17
+ readonly state: AgentLifecycleState;
18
+ readonly endpointReachable: boolean | null;
19
+ readonly canAutoRestart: boolean;
20
+ readonly pid?: number;
21
+ readonly endpoint?: string;
22
+ readonly message: string;
23
+ readonly browserRuntime?: BrowserRuntimeInspectionBoundary;
24
+ }
25
+ export interface AgentLifecycleBaselineState {
26
+ readonly agent: RegisteredAgent;
27
+ readonly pid?: number;
28
+ readonly runtimeIdentity?: AgentLifecycleRuntimeIdentity;
29
+ }
30
+ export type AgentLifecycleRuntimeIdentity = ManagedAgentRuntimeIdentity;
31
+ /**
32
+ * 保存配置前捕获的常驻状态。配置应用只会重启这里记录为运行中的 core-managed Agent。
33
+ */
34
+ export interface AgentLifecycleBaseline {
35
+ readonly dataDir: string;
36
+ readonly capturedAt: string;
37
+ readonly agents: Readonly<Record<string, AgentLifecycleBaselineState>>;
38
+ }
39
+ export interface AgentActivationResultItem {
40
+ readonly effect: ConfigActivationEffect;
41
+ readonly status: AgentActivationStatus;
42
+ readonly message: string;
43
+ readonly pid?: number;
44
+ }
45
+ export interface AgentActivationResult {
46
+ readonly success: boolean;
47
+ readonly requiresManualAction: boolean;
48
+ readonly restartedAgentNames: readonly string[];
49
+ readonly items: readonly AgentActivationResultItem[];
50
+ }
51
+ export interface AgentLifecycleCollaborators {
52
+ readonly readAgents: (dataDir: string) => ReadonlyArray<RegisteredAgent>;
53
+ readonly updateStatus: (dataDir: string, agentName: string, status: AgentStatus) => void;
54
+ readonly getPid: (dataDir: string, agentName: string) => number | undefined;
55
+ readonly inspectRuntime: (agent: RegisteredAgent, dataDir: string) => ManagedAgentRuntimeInspection;
56
+ readonly probe: (agent: RegisteredAgent, options?: {
57
+ readonly timeoutMs?: number;
58
+ }) => Promise<void>;
59
+ readonly start: (agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string>>, options?: {
60
+ readonly lifecycleLock?: AgentLifecycleLock;
61
+ }) => number;
62
+ readonly stopGracefully: (dataDir: string, agentName: string, options?: {
63
+ readonly timeoutMs?: number;
64
+ readonly intervalMs?: number;
65
+ readonly expectedIdentity?: AgentLifecycleRuntimeIdentity;
66
+ readonly lifecycleLock?: AgentLifecycleLock;
67
+ }) => Promise<boolean>;
68
+ readonly waitUntilReady: (agent: RegisteredAgent, options?: {
69
+ readonly startupTimeoutMs?: number;
70
+ readonly probeTimeoutMs?: number;
71
+ readonly intervalMs?: number;
72
+ }) => Promise<void>;
73
+ readonly resolveAgentEnv: (config: RollConfig, agentName: string) => Readonly<Record<string, string>> | undefined;
74
+ readonly acquireLifecycleLock: (dataDir: string, agentName: string) => AgentLifecycleLock;
75
+ }
76
+ export interface AgentInspectionOptions {
77
+ readonly probeTimeoutMs?: number;
78
+ }
79
+ /**
80
+ * CLI 与本地 UI 可共同复用的 Agent 状态检查和配置生效服务。
81
+ *
82
+ * 此服务只管理 Agent 进程/MCP endpoint;即使 browser-use-agent 显示 running,
83
+ * 也不代表 Chrome 已启动,浏览器实例仍由首次浏览器工具调用懒启动。
84
+ */
85
+ export declare class AgentLifecycleService {
86
+ readonly dataDir: string;
87
+ private readonly collaborators;
88
+ constructor(dataDir: string, collaborators?: Partial<AgentLifecycleCollaborators>);
89
+ inspectAll(options?: AgentInspectionOptions): Promise<readonly AgentLifecycleInspection[]>;
90
+ captureBaseline(): AgentLifecycleBaseline;
91
+ /** 显式重启一个当前正在运行的 core-managed Agent;不会把 stopped Agent 启动起来。 */
92
+ restartRunningAgent(agentName: string, effectiveConfig: RollConfig): Promise<AgentActivationResultItem>;
93
+ applyActivation(effects: readonly ConfigActivationEffect[], baseline: AgentLifecycleBaseline, effectiveConfig: RollConfig): Promise<AgentActivationResult>;
94
+ private inspectAgent;
95
+ private applyRestartEffect;
96
+ private restartCoreManagedAgent;
97
+ }
@@ -0,0 +1 @@
1
+ import{getAgentEnv as t}from"../config/helpers.js";import{acquireAgentLifecycleLock as e,getAgentPid as a,inspectManagedAgentRuntime as n,probeAgentEndpoint as r,startAgent as i,stopAgentGracefully as s,waitForAgentReady as o}from"./process-manager.js";import{AgentStore as c}from"./store.js";const d="browser-use-agent";export const AGENT_LIFECYCLE_STATES=["ready-on-demand","running","stopped","unreachable","external-online","external-unreachable"];export const AGENT_ACTIVATION_STATUSES=["deferred","restarted","kept-stopped","next-invocation","manual","runtime-changed","failed"];const l={readAgents:t=>new c(t).list(),updateStatus:(t,e,a)=>{new c(t).updateStatus(e,a)},getPid:a,inspectRuntime:n,probe:r,start:i,stopGracefully:s,waitUntilReady:o,resolveAgentEnv:t,acquireLifecycleLock:e};export class AgentLifecycleService{dataDir;collaborators;constructor(t,e={}){this.dataDir=t,this.collaborators={...l,...e}}async inspectAll(t={}){const e=this.collaborators.readAgents(this.dataDir);return Promise.all(e.map(e=>this.inspectAgent(e,t)))}captureBaseline(){const t={};for(const e of this.collaborators.readAgents(this.dataDir)){const a="core-managed"===e.runtime.ownership?this.collaborators.inspectRuntime(e,this.dataDir):void 0,n=a?.pid,r=void 0===a?void 0:u(e,a);t[e.skill.name]={agent:e,...void 0!==n?{pid:n}:{},...void 0!==r?{runtimeIdentity:r}:{}}}return{dataDir:this.dataDir,capturedAt:(new Date).toISOString(),agents:t}}async restartRunningAgent(t,e){const a={kind:"restart-agent",paths:[],title:`重启 ${t}`,description:"重启当前运行中的 core-managed Agent。",agentName:t,requiresConfirmation:!0},n=this.captureBaseline();return this.applyRestartEffect(a,n,e,!1)}async applyActivation(t,e,a){if(e.dataDir!==this.dataDir)throw new Error(`Agent lifecycle baseline dataDir mismatch: expected ${this.dataDir}, received ${e.dataDir}`);const n=t.some(t=>"manual"===t.kind&&t.paths.some(y)),r=new Set,i=[];for(const s of t)if("restart-agent"!==s.kind)i.push(k(s));else{if(void 0!==s.agentName){if(r.has(s.agentName))continue;r.add(s.agentName)}i.push(await this.applyRestartEffect(s,e,a,n))}const s=new Set(["manual","runtime-changed","failed"]);return{success:i.every(t=>"failed"!==t.status),requiresManualAction:i.some(t=>s.has(t.status)),restartedAgentNames:i.flatMap(t=>"restarted"===t.status&&void 0!==t.effect.agentName?[t.effect.agentName]:[]),items:i}}async inspectAgent(t,e){const a=m(t);switch(t.runtime.ownership){case"on-demand":return{...a,state:"ready-on-demand",endpointReachable:null,canAutoRestart:!1,message:"按需模式:配置会在下一次 run/ask 调用时生效,无常驻进程可重启。"};case"external-managed":try{return await this.collaborators.probe(t,g(e)),{...a,state:"external-online",endpointReachable:!0,canAutoRestart:!1,message:t.skill.name===d?"browser-use-agent 的外部 MCP endpoint 可连接;这不代表 Chrome 已启动。":"外部托管 endpoint 可连接;启动、停止和重启仍由外部系统负责。",...f(t)}}catch{return{...a,state:"external-unreachable",endpointReachable:!1,canAutoRestart:!1,message:"外部托管 endpoint 不可连接;请检查外部服务状态和 Agent 日志。",...f(t)}}case"core-managed":{const n=this.collaborators.inspectRuntime(t,this.dataDir),r=n.pid;if(void 0===r)return{...a,state:"stopped",endpointReachable:null,canAutoRestart:!1,message:"Agent 当前已停止;保存配置不会把它自动启动。"};if(void 0===u(t,n))return{...a,state:"unreachable",endpointReachable:null,canAutoRestart:!1,pid:r,message:"Agent PID 存在,但 runtime 身份无法安全验证;已禁用自动停止/重启,请按诊断提示人工处理。",...f(t)};try{return await this.collaborators.probe(t,g(e)),{...a,state:"running",endpointReachable:!0,canAutoRestart:!0,pid:r,message:b(t),...f(t)}}catch{return{...a,state:"unreachable",endpointReachable:!1,canAutoRestart:!0,pid:r,message:"Agent 进程存在,但 MCP endpoint 不可连接;请检查 Agent 日志。",...f(t)}}}}}async applyRestartEffect(t,e,a,n){const r=t.agentName;if(void 0===r)return v(t,"manual","生效计划没有提供 Agent 名称,无法自动重启。");if(n)return v(t,"manual","`agents.dataDir` 已变更;先停止 Agent 并人工迁移 PID、日志和注册数据,再执行重启。");const i=e.agents[r];if(void 0===i)return v(t,"manual",`保存前的 Agent 列表中没有 ${r},不会自动启动。`);switch(i.agent.runtime.ownership){case"on-demand":return v(t,"next-invocation",`${r} 为按需模式,配置会在下一次 run/ask 调用时生效。`);case"external-managed":return v(t,"manual",`${r} 由外部系统管理,请在外部完成重启。`)}return void 0===i.pid?v(t,"kept-stopped",`${r} 保存前已停止,保持停止;下次手动启动时会读取新配置。`):void 0===i.runtimeIdentity?v(t,"runtime-changed",`${r} 保存前的进程缺少可验证的 runtime sidecar;为避免停止无关进程,未自动应用。`):this.restartCoreManagedAgent(t,i.agent,i.runtimeIdentity,a)}async restartCoreManagedAgent(t,e,a,n){const r=a.pid;let i,s,o;try{o=this.collaborators.acquireLifecycleLock(this.dataDir,e.skill.name);const c=this.collaborators.inspectRuntime(e,this.dataDir);if(!p(u(e,c),a)){const a=c.pid;return v(t,"runtime-changed",void 0===a?`${e.skill.name} 在保存后已被停止,为避免意外重新启动,未自动应用。`:a!==r?`${e.skill.name} 的 PID 已从 ${String(r)} 变为 ${String(a)},为避免停止新进程,未自动应用。`:`${e.skill.name} 的 runtime sidecar 已缺失、失效或不再对应保存前的进程;为避免停止无关进程,未自动应用。`)}if(!await this.collaborators.stopGracefully(this.dataDir,e.skill.name,{expectedIdentity:a,lifecycleLock:o}))return v(t,"runtime-changed",`${e.skill.name} 在应用前已不再运行,为避免意外启动,未自动应用。`);this.collaborators.updateStatus(this.dataDir,e.skill.name,"starting");const d=this.collaborators.resolveAgentEnv(n,e.skill.name);if(i=this.collaborators.start(e,this.dataDir,d,{lifecycleLock:o}),s=u(e,this.collaborators.inspectRuntime(e,this.dataDir)),s?.pid!==i)throw new Error("Started Agent runtime identity could not be verified.");return await this.collaborators.waitUntilReady(e),this.collaborators.updateStatus(this.dataDir,e.skill.name,"online"),v(t,"restarted",A(e),i)}catch{let a=!1;if(void 0!==i){const t=this.collaborators.inspectRuntime(e,this.dataDir),n=u(e,t);let r=t.pid;void 0!==s&&p(n,s)&&(await this.collaborators.stopGracefully(this.dataDir,e.skill.name,{expectedIdentity:s,...void 0!==o?{lifecycleLock:o}:{}}).catch(()=>!1),r=this.collaborators.getPid(this.dataDir,e.skill.name)),a=void 0!==r&&r!==i}return void 0===o||a||this.collaborators.updateStatus(this.dataDir,e.skill.name,"error"),v(t,"failed",`${e.skill.name} 重启失败;请检查 Agent 日志。`)}finally{o?.release()}}}function u(t,e){const{pid:a,sidecar:n}=e;if(!(void 0===a||void 0===n||e.issues.length>0||n.agentName!==t.skill.name||n.pid!==a||n.coreVersion!==e.expectedCoreVersion)){if("streamable-http"===t.transport.type){if(e.expectedEndpoint!==t.transport.endpoint||n.endpoint!==t.transport.endpoint)return}else if(void 0!==n.endpoint||void 0!==e.expectedEndpoint)return;return{pid:a,processStartToken:n.processStartToken,startedAt:n.startedAt}}}function p(t,e){return t?.pid===e.pid&&t.processStartToken===e.processStartToken&&t.startedAt===e.startedAt}function m(t){return{agentName:t.skill.name,ownership:t.runtime.ownership,transport:t.transport.type,..."streamable-http"===t.transport.type?{endpoint:h(t.transport.endpoint)}:{}}}function h(t){try{const e=new URL(t);return e.username="",e.password="",e.search="",e.hash="",e.toString()}catch{return"[invalid endpoint]"}}function g(t){return void 0!==t.probeTimeoutMs?{timeoutMs:t.probeTimeoutMs}:{}}function f(t){return t.skill.name!==d?{}:{browserRuntime:{state:"not-inspected",message:"这里只检查 browser-use-agent 的 MCP endpoint;不代表 Chrome 已启动,Chrome 仍在首次浏览器工具调用时懒启动。"}}}function b(t){return t.skill.name===d?"browser-use-agent 的 MCP endpoint 已在线;这不代表 Chrome 已启动。":"Agent 进程与 MCP endpoint 均在线。"}function A(t){return t.skill.name===d?"browser-use-agent 的 MCP endpoint 已重启并在线;Chrome 仍会在首次浏览器工具调用时懒启动。":`${t.skill.name} 已重启并在线。`}function y(t){return"agents"===t[0]&&"dataDir"===t[1]}function k(t){switch(t.kind){case"next-command":return v(t,"deferred","配置已保存,后续 Roll 命令会重新加载。 ");case"next-chat":return v(t,"deferred","配置已保存,新建 roll chat 会话后生效。 ");case"manual":return v(t,"manual",t.description);case"restart-agent":return v(t,"manual","未执行 Agent 重启。 ")}}function v(t,e,a,n){return{effect:t,status:e,message:a.trim(),...void 0!==n?{pid:n}:{}}}
@@ -1 +1 @@
1
- import{readFileSync as t,existsSync as r,realpathSync as n}from"node:fs";import{readJsonFile as e}from"./json-file.js";import{resolve as o,sep as i}from"node:path";import a from"gray-matter";import{z as s}from"zod";import{parse as p}from"yaml";import{createDefaultRuntimeForTransport as l}from"../types/agent.js";const d="SKILL.md",m="package.json",c=s.object({name:s.string().min(1),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional()}),u=s.object({required:s.array(c).optional(),optional:s.array(c).optional()}),f=s.object({required:s.boolean().optional(),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional()}),h=s.object({env:s.record(f).optional()});export function discoverAgent(n){const e=o(n),i=o(e,d);if(!r(i))throw new Error(`SKILL.md not found in ${e}`);const s=t(i,"utf-8"),{data:p,content:m}=a(s),c=j(e),u=p,f=u.name,h=u.description;if("string"!=typeof f||0===f.length)throw new Error(`SKILL.md missing required field "name" in ${i}`);if("string"!=typeof h||0===h.length)throw new Error(`SKILL.md missing required field "description" in ${i}`);const g=u.metadata??{},w={};for(const[t,r]of Object.entries(g))w[t]=String(r);const v="string"==typeof u.license?u.license:void 0,b="string"==typeof u.compatibility?u.compatibility:void 0,$=y(e,w,i),S={name:f,description:h,...v?{license:v}:{},...b?{compatibility:b}:{},metadata:w,...$?{env:$}:{}},I=c?E(c):void 0;if(I&&q(w)){if(!x(L(w),I.transport))throw new Error(`Conflicting runtime metadata in ${i}: package.json#rollAgent and SKILL.md metadata disagree`)}const K=I?.transport??L(w);return{skill:S,transport:K,runtime:I?.runtime??l(K),skillPath:i,skillBody:m.trim()}}function g(t,r){if(void 0===t)return;if(A(t)&&"env"in t)return w(t,r);const n=u.safeParse(t);if(!n.success){const t=n.error.issues.map(t=>`${t.path.length>0?`env.${t.path.join(".")}`:"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const e=n.data;return e.required||e.optional?{...e.required?{required:e.required.map(b)}:{},...e.optional?{optional:e.optional.map(b)}:{}}:void 0}function w(t,r){const n=h.safeParse(t);if(!n.success){const t=n.error.issues.map(t=>`${t.path.length>0?t.path.join("."):"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const e=n.data.env;if(!e)return;const o=[],i=[];for(const[t,n]of Object.entries(e)){if(0===t.length)throw new Error(`SKILL.md has invalid "env" declaration in ${r}: env key is empty`);const e=$(t,n);!0===n.required?o.push(e):i.push(e)}return 0!==o.length||0!==i.length?{...o.length>0?{required:o}:{},...i.length>0?{optional:i}:{}}:void 0}function y(e,i,a){const s=i["roll-env-file"];if(!s)return;const l=o(e,s);if(!v(e,l))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);if(!r(l))throw new Error(`SKILL.md roll-env-file not found: ${l}`);const d=n(e),m=n(l);if(!v(d,m))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);let c;try{c=p(t(m,"utf-8"))}catch(t){throw new Error(`Failed to parse roll-env-file for ${a}: ${t instanceof Error?t.message:String(t)}`)}return g(c,m)}function v(t,r){return r===t||r.startsWith(`${t}${i}`)}function b(t){return{name:t.name,...t.purpose?{purpose:t.purpose}:{},...t.example?{example:t.example}:{},...t.default?{default:t.default}:{}}}function $(t,r){return{name:t,...r.purpose?{purpose:r.purpose}:{},...r.example?{example:r.example}:{},...r.default?{default:r.default}:{}}}function L(t){if("streamable-http"===("streamable-http"===t["roll-transport"]?"streamable-http":"stdio")){const r=t["roll-endpoint"];if(!r)throw new Error('SKILL.md declares streamable-http transport but missing "roll-endpoint"');return{type:"streamable-http",endpoint:r}}const r=(t["roll-command"]??"node --experimental-strip-types src/index.ts").split(/\s+/),n=r[0],e=r.slice(1);if(!n)throw new Error("Invalid roll-command in SKILL.md");return e.length>0?{type:"stdio",command:n,args:e}:{type:"stdio",command:n}}function j(t){const n=o(t,m);if(!r(n))return;let i;try{i=e(n)}catch{throw new Error(`Invalid package.json in ${t}`)}if(!A(i)||!A(i.rollAgent))return;const a=i.rollAgent,s=A(a.runtime)?a.runtime:void 0;if(!s)throw new Error(`package.json#rollAgent.runtime is required in ${n}`);const p={runtime:{ownership:K(s.ownership)??"",transport:K(s.transport)??""}};if(A(a.start)){const t=K(a.start.command),r=k(a.start.args);(t||r)&&(p.start={...t?{command:t}:{},...r?{args:r}:{}})}if(A(a.endpoint)){const t=K(a.endpoint.url),r=K(a.endpoint.path),n="number"==typeof a.endpoint.port?a.endpoint.port:void 0;(t||r||void 0!==n)&&(p.endpoint={...t?{url:t}:{},...r?{path:r}:{},...void 0!==n?{port:n}:{}})}if(A(a.setup)&&A(a.setup.playwright)){const t=k(a.setup.playwright.browsers);t&&t.length>0&&(p.setup={playwright:{browsers:t}})}return p}function E(t){const{ownership:r,transport:n}=t.runtime;if("on-demand"===r&&"stdio"===n){const r=t.start?.command;if(!r)throw new Error("package.json#rollAgent.start.command is required for stdio runtime");const n=t.start?.args;return{transport:n?{type:"stdio",command:r,args:n}:{type:"stdio",command:r},runtime:{ownership:"on-demand"}}}if("core-managed"===r&&"streamable-http"===n){const r=t.start?.command,n=S(t),e=t.endpoint?.path,o=t.endpoint?.port;if(!r||!e||void 0===o)throw new Error("package.json#rollAgent for core-managed streamable-http requires start.command, endpoint.path and endpoint.port");const i=t.start?.args,a=t.setup?.playwright?.browsers;return{transport:{type:"streamable-http",endpoint:n},runtime:{ownership:"core-managed",start:i?{command:r,args:i}:{command:r},endpoint:{path:e,port:o},...a&&a.length>0?{setup:{playwright:{browsers:a}}}:{}}}}if("external-managed"===r&&"streamable-http"===n)return{transport:{type:"streamable-http",endpoint:S(t)},runtime:{ownership:"external-managed"}};throw new Error(`Unsupported package.json#rollAgent runtime combination: ${r}/${n}`)}function S(t){const r=t.endpoint?.url;if(r)return r;const n=t.endpoint?.path,e=t.endpoint?.port;if(!n||void 0===e)throw new Error("package.json#rollAgent.streamable-http requires endpoint.url or endpoint.path + endpoint.port");return I(n,e)}function I(t,r){const n=t.startsWith("/")?t:`/${t}`;return`http://127.0.0.1:${String(r)}${n}`}function q(t){return"string"==typeof t["roll-transport"]||"string"==typeof t["roll-endpoint"]||"string"==typeof t["roll-command"]}function x(t,r){if(t.type!==r.type)return!1;if("streamable-http"===t.type&&"streamable-http"===r.type)return t.endpoint===r.endpoint;if("stdio"!==t.type||"stdio"!==r.type)return!1;const n=t.args??[],e=r.args??[];return t.command===r.command&&n.join("\0")===e.join("\0")}function K(t){return"string"==typeof t&&t.length>0?t:void 0}function k(t){if(!Array.isArray(t))return;const r=t.filter(t=>"string"==typeof t);return r.length>0?r:void 0}function A(t){return"object"==typeof t&&null!==t}
1
+ import{readFileSync as t,existsSync as r,realpathSync as e}from"node:fs";import{readJsonFile as n}from"./json-file.js";import{resolve as o,sep as i}from"node:path";import a from"gray-matter";import{z as s}from"zod";import{parse as p}from"yaml";import{AGENT_ENV_VALUE_TYPES as l,createDefaultRuntimeForTransport as d}from"../types/agent.js";const c="SKILL.md",m="package.json",u=s.object({name:s.string().min(1),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional(),type:s.enum(l).optional(),secret:s.boolean().optional(),configurable:s.boolean().optional(),sourcePath:s.array(s.string().min(1)).min(1).optional()}),f=s.object({required:s.array(u).optional(),optional:s.array(u).optional()}),g=s.object({required:s.boolean().optional(),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional(),type:s.enum(l).optional(),secret:s.boolean().optional(),configurable:s.boolean().optional(),sourcePath:s.array(s.string().min(1)).min(1).optional()}),h=s.object({env:s.record(g).optional()});export function discoverAgent(e){const n=o(e),i=o(n,c);if(!r(i))throw new Error(`SKILL.md not found in ${n}`);const s=t(i,"utf-8"),{data:p,content:l}=a(s),m=E(n),u=p,f=u.name,g=u.description;if("string"!=typeof f||0===f.length)throw new Error(`SKILL.md missing required field "name" in ${i}`);if("string"!=typeof g||0===g.length)throw new Error(`SKILL.md missing required field "description" in ${i}`);const h=u.metadata??{},y={};for(const[t,r]of Object.entries(h))y[t]=String(r);const w="string"==typeof u.license?u.license:void 0,b="string"==typeof u.compatibility?u.compatibility:void 0,$=v(n,y,i),L={name:f,description:g,...w?{license:w}:{},...b?{compatibility:b}:{},metadata:y,...$?{env:$}:{}},I=m?S(m):void 0;if(I&&x(y)){if(!K(j(y),I.transport))throw new Error(`Conflicting runtime metadata in ${i}: package.json#rollAgent and SKILL.md metadata disagree`)}const q=I?.transport??j(y);return{skill:L,transport:q,runtime:I?.runtime??d(q),skillPath:i,skillBody:l.trim()}}function y(t,r){if(void 0===t)return;if(P(t)&&"env"in t)return w(t,r);const e=f.safeParse(t);if(!e.success){const t=e.error.issues.map(t=>`${t.path.length>0?`env.${t.path.join(".")}`:"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const n=e.data;return n.required||n.optional?{...n.required?{required:n.required.map($)}:{},...n.optional?{optional:n.optional.map($)}:{}}:void 0}function w(t,r){const e=h.safeParse(t);if(!e.success){const t=e.error.issues.map(t=>`${t.path.length>0?t.path.join("."):"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const n=e.data.env;if(!n)return;const o=[],i=[];for(const[t,e]of Object.entries(n)){if(0===t.length)throw new Error(`SKILL.md has invalid "env" declaration in ${r}: env key is empty`);const n=L(t,e);!0===e.required?o.push(n):i.push(n)}return 0!==o.length||0!==i.length?{...o.length>0?{required:o}:{},...i.length>0?{optional:i}:{}}:void 0}function v(n,i,a){const s=i["roll-env-file"];if(!s)return;const l=o(n,s);if(!b(n,l))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);if(!r(l))throw new Error(`SKILL.md roll-env-file not found: ${l}`);const d=e(n),c=e(l);if(!b(d,c))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);let m;try{m=p(t(c,"utf-8"))}catch(t){throw new Error(`Failed to parse roll-env-file for ${a}: ${t instanceof Error?t.message:String(t)}`)}return y(m,c)}function b(t,r){return r===t||r.startsWith(`${t}${i}`)}function $(t){return{name:t.name,...t.purpose?{purpose:t.purpose}:{},...t.example?{example:t.example}:{},...t.default?{default:t.default}:{},...void 0!==t.type?{type:t.type}:{},...void 0!==t.secret?{secret:t.secret}:{},...void 0!==t.configurable?{configurable:t.configurable}:{},...void 0!==t.sourcePath?{sourcePath:t.sourcePath}:{}}}function L(t,r){return{name:t,...r.purpose?{purpose:r.purpose}:{},...r.example?{example:r.example}:{},...r.default?{default:r.default}:{},...void 0!==r.type?{type:r.type}:{},...void 0!==r.secret?{secret:r.secret}:{},...void 0!==r.configurable?{configurable:r.configurable}:{},...void 0!==r.sourcePath?{sourcePath:r.sourcePath}:{}}}function j(t){if("streamable-http"===("streamable-http"===t["roll-transport"]?"streamable-http":"stdio")){const r=t["roll-endpoint"];if(!r)throw new Error('SKILL.md declares streamable-http transport but missing "roll-endpoint"');return{type:"streamable-http",endpoint:r}}const r=(t["roll-command"]??"node --experimental-strip-types src/index.ts").split(/\s+/),e=r[0],n=r.slice(1);if(!e)throw new Error("Invalid roll-command in SKILL.md");return n.length>0?{type:"stdio",command:e,args:n}:{type:"stdio",command:e}}function E(t){const e=o(t,m);if(!r(e))return;let i;try{i=n(e)}catch{throw new Error(`Invalid package.json in ${t}`)}if(!P(i)||!P(i.rollAgent))return;const a=i.rollAgent,s=P(a.runtime)?a.runtime:void 0;if(!s)throw new Error(`package.json#rollAgent.runtime is required in ${e}`);const p={runtime:{ownership:k(s.ownership)??"",transport:k(s.transport)??""}};if(P(a.start)){const t=k(a.start.command),r=A(a.start.args);(t||r)&&(p.start={...t?{command:t}:{},...r?{args:r}:{}})}if(P(a.endpoint)){const t=k(a.endpoint.url),r=k(a.endpoint.path),e="number"==typeof a.endpoint.port?a.endpoint.port:void 0;(t||r||void 0!==e)&&(p.endpoint={...t?{url:t}:{},...r?{path:r}:{},...void 0!==e?{port:e}:{}})}if(P(a.setup)&&P(a.setup.playwright)){const t=A(a.setup.playwright.browsers);t&&t.length>0&&(p.setup={playwright:{browsers:t}})}return p}function S(t){const{ownership:r,transport:e}=t.runtime;if("on-demand"===r&&"stdio"===e){const r=t.start?.command;if(!r)throw new Error("package.json#rollAgent.start.command is required for stdio runtime");const e=t.start?.args;return{transport:e?{type:"stdio",command:r,args:e}:{type:"stdio",command:r},runtime:{ownership:"on-demand"}}}if("core-managed"===r&&"streamable-http"===e){const r=t.start?.command,e=I(t),n=t.endpoint?.path,o=t.endpoint?.port;if(!r||!n||void 0===o)throw new Error("package.json#rollAgent for core-managed streamable-http requires start.command, endpoint.path and endpoint.port");const i=t.start?.args,a=t.setup?.playwright?.browsers;return{transport:{type:"streamable-http",endpoint:e},runtime:{ownership:"core-managed",start:i?{command:r,args:i}:{command:r},endpoint:{path:n,port:o},...a&&a.length>0?{setup:{playwright:{browsers:a}}}:{}}}}if("external-managed"===r&&"streamable-http"===e)return{transport:{type:"streamable-http",endpoint:I(t)},runtime:{ownership:"external-managed"}};throw new Error(`Unsupported package.json#rollAgent runtime combination: ${r}/${e}`)}function I(t){const r=t.endpoint?.url;if(r)return r;const e=t.endpoint?.path,n=t.endpoint?.port;if(!e||void 0===n)throw new Error("package.json#rollAgent.streamable-http requires endpoint.url or endpoint.path + endpoint.port");return q(e,n)}function q(t,r){const e=t.startsWith("/")?t:`/${t}`;return`http://127.0.0.1:${String(r)}${e}`}function x(t){return"string"==typeof t["roll-transport"]||"string"==typeof t["roll-endpoint"]||"string"==typeof t["roll-command"]}function K(t,r){if(t.type!==r.type)return!1;if("streamable-http"===t.type&&"streamable-http"===r.type)return t.endpoint===r.endpoint;if("stdio"!==t.type||"stdio"!==r.type)return!1;const e=t.args??[],n=r.args??[];return t.command===r.command&&e.join("\0")===n.join("\0")}function k(t){return"string"==typeof t&&t.length>0?t:void 0}function A(t){if(!Array.isArray(t))return;const r=t.filter(t=>"string"==typeof t);return r.length>0?r:void 0}function P(t){return"object"==typeof t&&null!==t}
@@ -1 +1 @@
1
- import{existsSync as e,mkdirSync as t,rmSync as s,statSync as r}from"node:fs";import{resolve as a}from"node:path";import{buildNpmRetryPolicy as n,formatPackageManagerError as i,npmInstallNetworkArgs as o,runPackageManagerWithRetry as l}from"../cli/utils/package-manager.js";import{inspectAgentEnvRequirements as g}from"../config/helpers.js";import{discoverAgent as m}from"./discovery.js";import{startAgent as p,stopAgentGracefully as c,waitForAgentReady as u}from"./process-manager.js";import{runAgentSetup as d}from"./runtime-setup.js";import{parsePackageName as f,readInstalledPackageManifest as k,resolveInstalledPackageRoot as y,sanitizeInstallId as h}from"./source.js";import{AgentStore as w}from"./store.js";export const INSTALL_AGENT_STEPS=["resolve","download","discover","setup","register","start"];function v(e){return o({...e.registry?{registry:e.registry}:{},fetchRetries:e.fetchRetries,preferOffline:e.preferOffline})}function S(e){return e.startsWith("git@")||e.startsWith("git+")||e.startsWith("github:")||e.startsWith("gitlab:")||e.startsWith("bitbucket:")||e.endsWith(".git")}function $(t){const s=a(t);return e(s)&&r(s).isDirectory()}function C(e){return e instanceof Error?e.message:String(e)}function A(e){return e.source?.type??"unknown"}function D(e,t,s){return{ok:!1,step:"register",message:`Agent "${e}" 已通过 ${t} 来源注册。为避免覆盖本地/Git Agent,默认不会替换为 npm 安装。请先运行 \`roll agent remove ${e}\`,或确认风险后使用 \`roll agent install ${s} --force\`。`,retryCommand:`roll agent install ${s} --force`}}function j(e,t){return void 0!==e&&"installed-package"!==e.source?.type&&!t}export async function installAgent(r,o){const R=o.report??(()=>{}),B=o.collaborators??{},b=B.runInstall??l,M=B.discover??m,N=B.runSetup??d,T=B.start??p,W=B.waitReady??u,x=B.stopGracefully??c,E=B.resolvePackageRoot??y,L=B.readManifest??k,{packageSpec:G}=r,I=r.autoStart??!0,O=r.replaceExisting??!1;if(S(G))return{ok:!1,step:"resolve",message:`Git URL 请使用 \`roll agent add ${G}\` 注册,不要使用 \`roll agent install\``};if($(G))return{ok:!1,step:"resolve",message:`本地源码目录请使用 \`roll agent add ${G}\` 注册,不要使用 \`roll agent install\``};const P=f(G),_=a(o.agentsConfig.dataDir,"installed",h(P)),q=o.store??new w(o.agentsConfig.dataDir),K=r.expectedSkillName,U=K?q.findByName(K):void 0;if(U&&j(U,O))return D(U.skill.name,A(U),G);const V=e(_);V||t(_,{recursive:!0}),o.installConfig.registry&&R({type:"info",message:`使用 npm registry: ${o.installConfig.registry}(roll.config.yaml install.registry)`}),R({type:"step",step:"download",message:`安装 ${G}...`});const z={command:"npm",args:["install","--prefix",_,G,...v(o.installConfig)]};try{await b(z,{timeout:o.installConfig.networkTimeoutMs},{...n(o.installConfig.fetchRetries),onRetry:({attempt:e,delayMs:t})=>{R({type:"retry",attempt:e,delayMs:t})}})}catch(e){return{ok:!1,step:"download",message:`安装失败: ${i(z,e)}`}}const F=E(_,P);if(!e(F))return{ok:!1,step:"discover",message:`安装完成但未找到包目录: ${F}`};const H=L(F);let J;R({type:"step",step:"discover",message:"解析已安装 Agent 的 SKILL.md..."});try{J=M(F)}catch(e){return{ok:!1,step:"discover",message:C(e)}}const Q={skill:J.skill,transport:J.transport,runtime:J.runtime,installPath:F,registeredAt:(new Date).toISOString(),status:"idle",source:{type:"installed-package",packageName:H?.name??P,packageSpec:G,installDir:_,...H?.version?{installedVersion:H.version}:{}},...J.skillBody.length>0?{skillBody:J.skillBody}:{}},X=q.findByName(J.skill.name);if(X&&j(X,O))return V||s(_,{recursive:!0,force:!0}),D(J.skill.name,A(X),G);"core-managed"===Q.runtime.ownership&&Q.runtime.setup?.playwright&&!r.skipBrowserSetup&&R({type:"info",message:`即将安装浏览器运行时 (${Q.runtime.setup.playwright.browsers.join(", ")}),这可能需要一些时间...`});const Y=await N(Q,{skipBrowserSetup:r.skipBrowserSetup??!1});Y.ok&&!Y.skipped?R({type:"success",message:Y.message}):Y.ok&&R({type:"info",message:Y.message});const Z="core-managed"===X?.runtime.ownership&&"online"===X.status;try{if("installed-package"===X?.source?.type||X&&O){if("installed-package"!==X.source?.type){const e=X.source?.type??"unknown";R({type:"info",message:`Agent "${J.skill.name}" 已通过 ${e} 来源注册,将替换为 npm 安装`})}q.replace(X.skill.name,Q)}else q.add(Q)}catch(e){return{ok:!1,step:"register",message:C(e)}}if(!Y.ok)return Z&&await x(o.agentsConfig.dataDir,Q.skill.name).catch(()=>{}),q.updateStatus(J.skill.name,"error"),{ok:!1,step:"setup",message:Y.message,...Y.retryCommand?{retryCommand:Y.retryCommand}:{}};const ee=g(Q.skill.name,J.skill.env,o.agentsConfig.env),te=ee?.missingRequired??[];let se=!1;const re="core-managed"===Q.runtime.ownership&&I,ae=re&&0===te.length;if(Z&&!ae&&(await x(o.agentsConfig.dataDir,Q.skill.name).catch(()=>{}),q.updateStatus(Q.skill.name,"idle")),re)if(te.length>0)R({type:"warn",message:`Agent "${Q.skill.name}" 缺少必填环境变量(${te.map(e=>e.name).join(", ")}),暂不启动。配置后运行 \`roll agent start ${Q.skill.name}\` 启动`});else{let e=!1;try{Z&&await x(o.agentsConfig.dataDir,Q.skill.name),q.updateStatus(Q.skill.name,"starting"),T(Q,o.agentsConfig.dataDir,o.getStartEnv(Q.skill.name)),e=!0,await W(Q,{startupTimeoutMs:15e3,probeTimeoutMs:2e3}),q.updateStatus(Q.skill.name,"online"),se=!0}catch(t){return e&&await x(o.agentsConfig.dataDir,Q.skill.name).catch(()=>{}),q.updateStatus(Q.skill.name,"error"),{ok:!1,step:"start",message:`Agent "${J.skill.name}" 已安装,但自动启动失败:${C(t)}`}}}return{ok:!0,agent:Q,envReport:ee,started:se}}
1
+ import{randomUUID as e}from"node:crypto";import{existsSync as t,lstatSync as r,mkdirSync as s,readFileSync as n,rmSync as a,statSync as i,writeFileSync as o}from"node:fs";import{basename as l,dirname as c,resolve as m}from"node:path";import{buildNpmRetryPolicy as g,formatPackageManagerError as p,npmInstallNetworkArgs as k,runPackageManagerWithRetry as u}from"../cli/utils/package-manager.js";import{inspectAgentEnvRequirements as f}from"../config/helpers.js";import{discoverAgent as d}from"./discovery.js";import{startAgent as y,stopAgentGracefully as h,waitForAgentReady as w}from"./process-manager.js";import{runAgentSetup as $}from"./runtime-setup.js";import{parsePackageName as v,readInstalledPackageManifest as S,resolveInstalledPackageRoot as D,sanitizeInstallId as A}from"./source.js";import{AgentStore as C}from"./store.js";export const INSTALL_AGENT_STEPS=["resolve","download","discover","setup","register","start"];const P=".install.lock";function T(e){return k({...e.registry?{registry:e.registry}:{},fetchRetries:e.fetchRetries,preferOffline:e.preferOffline})}function j(e){return e.startsWith("git@")||e.startsWith("git+")||e.startsWith("github:")||e.startsWith("gitlab:")||e.startsWith("bitbucket:")||e.endsWith(".git")}function B(e){const r=m(e);return t(r)&&i(r).isDirectory()}function b(e){return e instanceof Error?e.message:String(e)}function E(e,t){return"object"==typeof e&&null!==e&&"code"in e&&e.code===t}function N(e,t){try{e({type:"warn",message:t})}catch{}}function R(e){return m(c(e),`.${l(e)}${P}`)}function I(e,t){o(e,`${JSON.stringify(t)}\n`,{encoding:"utf8",flag:"wx",mode:384})}function O(e){try{const t=JSON.parse(n(e,"utf8"));return"object"==typeof t&&null!==t&&"token"in t&&"string"==typeof t.token?t.token:void 0}catch{return}}function x(e,r){const s=O(e.lockPath);if(void 0!==s)if(s===e.markerToken)try{a(e.lockPath)}catch(t){N(r,`清理安装锁失败: ${e.lockPath}(${b(t)})`)}else N(r,`安装锁已被其他任务替换,未执行清理: ${e.lockPath}`);else t(e.lockPath)&&N(r,`安装锁内容无法验证,未自动删除: ${e.lockPath}`)}function L(t,n){const a=e(),i=R(t),o=c(t);try{s(o,{recursive:!0}),I(i,{token:a,pid:process.pid,startedAt:(new Date).toISOString()})}catch(e){return{ok:!1,failure:{ok:!1,step:"resolve",message:E(e,"EEXIST")?`Agent 安装任务正在使用锁文件: ${i}。请先检查锁文件中的 pid,确认对应安装进程已结束后再手动删除该锁文件并重试;Roll 不会自动抢占疑似 stale lock。`:`无法创建 Agent 安装锁: ${b(e)}`}}}let l;try{l=r(t)}catch(e){if(!E(e,"ENOENT"))return x({installDir:t,lockPath:i,markerToken:a},n),{ok:!1,failure:{ok:!1,step:"resolve",message:`无法检查 Agent 安装目录: ${b(e)}`}}}if(void 0!==l)return l.isSymbolicLink()?(x({installDir:t,lockPath:i,markerToken:a},n),{ok:!1,failure:{ok:!1,step:"resolve",message:`Agent 安装目录不能是符号链接: ${t}`}}):l.isDirectory()?{ok:!0,workspace:{installDir:t,lockPath:i,markerToken:a,createdByInvocation:!1}}:(x({installDir:t,lockPath:i,markerToken:a},n),{ok:!1,failure:{ok:!1,step:"resolve",message:`Agent 安装路径已存在但不是目录: ${t}`}});try{s(t)}catch(e){return x({installDir:t,lockPath:i,markerToken:a},n),{ok:!1,failure:{ok:!1,step:"resolve",message:`无法创建 Agent 安装目录: ${b(e)}`}}}return{ok:!0,workspace:{installDir:t,lockPath:i,markerToken:a,createdByInvocation:!0}}}function M(e,t){if(e.createdByInvocation)if(O(e.lockPath)===e.markerToken)try{a(e.installDir,{recursive:!0,force:!0})}catch(r){N(t,`清理未完成的安装目录失败: ${e.installDir}(${b(r)})`)}else N(t,`安装目录锁不再属于当前任务,未删除目录: ${e.installDir}`)}function W(e){return e.source?.type??"unknown"}function G(e,t,r){return{ok:!1,step:"register",message:`Agent "${e}" 已通过 ${t} 来源注册。为避免覆盖本地/Git Agent,默认不会替换为 npm 安装。请先运行 \`roll agent remove ${e}\`,或确认风险后使用 \`roll agent install ${r} --force\`。`,retryCommand:`roll agent install ${r} --force`}}function J(e,t){return void 0!==e&&"installed-package"!==e.source?.type&&!t}export async function installAgent(e,r){const s=r.report??(()=>{}),n=r.collaborators??{},a=n.runInstall??u,i=n.discover??d,o=n.runSetup??$,l=n.start??y,c=n.waitReady??w,k=n.stopGracefully??h,P=n.resolvePackageRoot??D,E=n.readManifest??S,{packageSpec:N}=e,R=e.autoStart??!0,I=e.replaceExisting??!1;if(j(N))return{ok:!1,step:"resolve",message:`Git URL 请使用 \`roll agent add ${N}\` 注册,不要使用 \`roll agent install\``};if(B(N))return{ok:!1,step:"resolve",message:`本地源码目录请使用 \`roll agent add ${N}\` 注册,不要使用 \`roll agent install\``};const O=v(N),_=m(r.agentsConfig.dataDir,"installed",A(O)),q=r.store??new C(r.agentsConfig.dataDir),K=e.expectedSkillName,U=K?q.findByName(K):void 0;if(U&&J(U,I))return G(U.skill.name,W(U),N);const V=L(_,s);if(!V.ok)return V.failure;const X=V.workspace;try{r.installConfig.registry&&s({type:"info",message:`使用 npm registry: ${r.installConfig.registry}(roll.config.yaml install.registry)`}),s({type:"step",step:"download",message:`安装 ${N}...`});const n={command:"npm",args:["install","--prefix",_,N,...T(r.installConfig)]};try{await a(n,{timeout:r.installConfig.networkTimeoutMs},{...g(r.installConfig.fetchRetries),onRetry:({attempt:e,delayMs:t})=>{s({type:"retry",attempt:e,delayMs:t})}})}catch(e){return M(X,s),{ok:!1,step:"download",message:`安装失败: ${p(n,e)}`}}let m,u,d;try{m=P(_,O)}catch(e){throw M(X,s),e}if(!t(m))return M(X,s),{ok:!1,step:"discover",message:`安装完成但未找到包目录: ${m}`};try{u=E(m)}catch(e){throw M(X,s),e}s({type:"step",step:"discover",message:"解析已安装 Agent 的 SKILL.md..."});try{d=i(m)}catch(e){return M(X,s),{ok:!1,step:"discover",message:b(e)}}const y={skill:d.skill,transport:d.transport,runtime:d.runtime,installPath:m,registeredAt:(new Date).toISOString(),status:"idle",source:{type:"installed-package",packageName:u?.name??O,packageSpec:N,installDir:_,...u?.version?{installedVersion:u.version}:{}},...d.skillBody.length>0?{skillBody:d.skillBody}:{}},h=q.findByName(d.skill.name);if(h&&J(h,I))return M(X,s),G(d.skill.name,W(h),N);let w;"core-managed"===y.runtime.ownership&&y.runtime.setup?.playwright&&!e.skipBrowserSetup&&s({type:"info",message:`即将安装浏览器运行时 (${y.runtime.setup.playwright.browsers.join(", ")}),这可能需要一些时间...`});try{w=await o(y,{skipBrowserSetup:e.skipBrowserSetup??!1})}catch(e){throw M(X,s),e}w.ok&&!w.skipped?s({type:"success",message:w.message}):w.ok&&s({type:"info",message:w.message});const $="core-managed"===h?.runtime.ownership&&"online"===h.status;try{if("installed-package"===h?.source?.type||h&&I){if("installed-package"!==h.source?.type){const e=h.source?.type??"unknown";s({type:"info",message:`Agent "${d.skill.name}" 已通过 ${e} 来源注册,将替换为 npm 安装`})}if(!q.replace(h.skill.name,y))throw new Error(`Agent "${h.skill.name}" 在安装期间已被移除,请重试`)}else q.add(y)}catch(e){return M(X,s),{ok:!1,step:"register",message:b(e)}}if(!w.ok)return $&&await k(r.agentsConfig.dataDir,y.skill.name).catch(()=>{}),q.updateStatus(d.skill.name,"error"),{ok:!1,step:"setup",message:w.message,...w.retryCommand?{retryCommand:w.retryCommand}:{}};const v=f(y.skill.name,d.skill.env,r.agentsConfig.env),S=v?.missingRequired??[];let D=!1;const A="core-managed"===y.runtime.ownership&&R,C=A&&0===S.length;if($&&!C&&(await k(r.agentsConfig.dataDir,y.skill.name).catch(()=>{}),q.updateStatus(y.skill.name,"idle")),A)if(S.length>0)s({type:"warn",message:`Agent "${y.skill.name}" 缺少必填环境变量(${S.map(e=>e.name).join(", ")}),暂不启动。配置后运行 \`roll agent start ${y.skill.name}\` 启动`});else{let e=!1;try{$&&await k(r.agentsConfig.dataDir,y.skill.name),q.updateStatus(y.skill.name,"starting"),l(y,r.agentsConfig.dataDir,r.getStartEnv(y.skill.name)),e=!0,await c(y,{startupTimeoutMs:15e3,probeTimeoutMs:2e3}),q.updateStatus(y.skill.name,"online"),D=!0}catch(t){return e&&await k(r.agentsConfig.dataDir,y.skill.name).catch(()=>{}),q.updateStatus(y.skill.name,"error"),{ok:!1,step:"start",message:`Agent "${d.skill.name}" 已安装,但自动启动失败:${b(t)}`}}}return{ok:!0,agent:y,envReport:v,started:D}}finally{x(X,s)}}
@@ -0,0 +1,14 @@
1
+ declare const PROCESS_START_TOKEN_BRAND: unique symbol;
2
+ /** Opaque digest of an OS-reported process creation identity. */
3
+ export type ProcessStartToken = string & {
4
+ readonly [PROCESS_START_TOKEN_BRAND]: true;
5
+ };
6
+ export declare function isProcessStartToken(value: unknown): value is ProcessStartToken;
7
+ /**
8
+ * Reads an OS-owned process creation value and turns it into a stable opaque token.
9
+ *
10
+ * Returning `undefined` is intentional: callers must fail closed instead of treating a PID as
11
+ * identity when the platform cannot prove which process instance currently owns it.
12
+ */
13
+ export declare function readProcessStartToken(pid: number): ProcessStartToken | undefined;
14
+ export {};
@@ -0,0 +1 @@
1
+ import{createHash as t}from"node:crypto";import{spawnSync as r}from"node:child_process";import{readFileSync as n}from"node:fs";const o=/^pst-v1:[a-f0-9]{64}$/u,e=2e3;export function isProcessStartToken(t){return"string"==typeof t&&o.test(t)}export function readProcessStartToken(r){if(!Number.isInteger(r)||r<=0)return;const n=s(r);if(void 0===n)return;const o=`pst-v1:${t("sha256").update(n).digest("hex")}`;return isProcessStartToken(o)?o:void 0}function s(t){switch(process.platform){case"linux":case"android":return i(t);case"darwin":return c(t);case"win32":return u(t);default:return d(t)}}function i(t){try{const r=n(`/proc/${String(t)}/stat`,"utf-8"),o=r.lastIndexOf(")");if(o<0)return;const e=r.slice(o+1).trim().split(/\s+/u)[19];if(void 0===e||!/^\d+$/u.test(e))return;const s=n("/proc/sys/kernel/random/boot_id","utf-8").trim();if(0===s.length)return;return`linux:${s}:${e}`}catch{return}}function c(t){const r=a("/bin/ps",["-p",String(t),"-o","lstart="]),n=a("/usr/sbin/sysctl",["-n","kern.boottime"]);if(void 0!==r&&void 0!==n)return`darwin:${n}:${r}`}function u(t){const r=["-NoLogo","-NoProfile","-NonInteractive","-Command",`$p = Get-Process -Id ${String(t)} -ErrorAction Stop; $p.StartTime.ToUniversalTime().Ticks`],n=a("powershell.exe",r)??a("pwsh.exe",r);if(void 0!==n&&/^\d+$/u.test(n))return`win32:${n}`}function d(t){const r=a("ps",["-p",String(t),"-o","lstart="]);return void 0===r?void 0:`${process.platform}:${r}`}function a(t,n){try{const o=r(t,[...n],{encoding:"utf-8",env:{...process.env,LC_ALL:"C",LANG:"C"},shell:!1,timeout:e,windowsHide:!0}),s=o.stdout.trim();return 0===o.status&&void 0===o.error&&s.length>0?s:void 0}catch{return}}
@@ -1,12 +1,28 @@
1
+ import { type ProcessStartToken } from "./process-identity.ts";
1
2
  import type { RegisteredAgent } from "../types/agent.ts";
2
- declare const RUNTIME_SIDECAR_SCHEMA_VERSION: 1;
3
- export type ManagedAgentRuntimeIssueCode = "missing-sidecar" | "invalid-sidecar" | "orphan-sidecar" | "pid-mismatch" | "version-mismatch" | "endpoint-mismatch";
4
- export interface ManagedAgentRuntimeSidecar {
3
+ declare const RUNTIME_SIDECAR_SCHEMA_VERSION: 2;
4
+ export interface AgentLifecycleLock {
5
+ release(): void;
6
+ }
7
+ export declare class AgentLifecycleBusyError extends Error {
8
+ readonly code: "agent_lifecycle_busy";
9
+ constructor(agentName: string);
10
+ }
11
+ export declare class AgentRuntimeIdentityError extends Error {
12
+ readonly code: "agent_runtime_identity_unverifiable";
13
+ readonly pid: number;
14
+ constructor(agentName: string, pid: number, reason: string);
15
+ }
16
+ export type ManagedAgentRuntimeIssueCode = "missing-sidecar" | "invalid-sidecar" | "orphan-sidecar" | "agent-name-mismatch" | "pid-mismatch" | "process-identity-unavailable" | "process-identity-mismatch" | "version-mismatch" | "endpoint-mismatch";
17
+ export interface ManagedAgentRuntimeIdentity {
18
+ readonly pid: number;
19
+ readonly processStartToken: ProcessStartToken;
20
+ readonly startedAt: string;
21
+ }
22
+ export interface ManagedAgentRuntimeSidecar extends ManagedAgentRuntimeIdentity {
5
23
  readonly schemaVersion: typeof RUNTIME_SIDECAR_SCHEMA_VERSION;
6
24
  readonly agentName: string;
7
- readonly pid: number;
8
25
  readonly coreVersion: string;
9
- readonly startedAt: string;
10
26
  readonly endpoint?: string;
11
27
  }
12
28
  export interface ManagedAgentRuntimeIssue {
@@ -25,7 +41,7 @@ export interface ManagedAgentRuntimeInspection {
25
41
  export declare function cleanupOrphanAgentRuntimeMetadata(dataDir: string, agentName: string): boolean;
26
42
  /** Agent 日志文件路径 */
27
43
  export declare function getAgentLogPath(dataDir: string, agentName: string): string;
28
- /** 读取 Agent PID,如果不存在或进程已死则返回 undefined */
44
+ /** 只读检查 Agent PID;过期元数据必须在持有 lifecycle lock 的清理路径中删除。 */
29
45
  export declare function getAgentPid(dataDir: string, agentName: string): number | undefined;
30
46
  export declare function getRollCoreVersion(): string;
31
47
  export declare function writeAgentRuntimeSidecar(agent: RegisteredAgent, dataDir: string, pid: number): void;
@@ -46,11 +62,16 @@ export declare function waitForAgentReady(agent: RegisteredAgent, options?: {
46
62
  * - `stdio + on-demand`:保留给旧健康检查逻辑使用
47
63
  * - `streamable-http + core-managed`:作为本地常驻后台服务启动
48
64
  */
49
- export declare function startAgent(agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string>>): number;
65
+ export declare function startAgent(agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string>>, options?: {
66
+ readonly lifecycleLock?: AgentLifecycleLock;
67
+ }): number;
50
68
  /** 停止一个后台运行的 Agent */
51
69
  export declare function stopAgent(dataDir: string, agentName: string): boolean;
52
70
  export declare function stopAgentGracefully(dataDir: string, agentName: string, options?: {
53
71
  readonly timeoutMs?: number;
54
72
  readonly intervalMs?: number;
73
+ readonly expectedIdentity?: ManagedAgentRuntimeIdentity;
74
+ readonly lifecycleLock?: AgentLifecycleLock;
55
75
  }): Promise<boolean>;
76
+ export declare function acquireAgentLifecycleLock(dataDir: string, agentName: string): AgentLifecycleLock;
56
77
  export {};
@@ -1 +1 @@
1
- import t from"cross-spawn";import{closeSync as e,existsSync as n,mkdirSync as r,openSync as i,readFileSync as o,unlinkSync as s,writeFileSync as a}from"node:fs";import{resolve as c,dirname as l}from"node:path";import{McpClientManager as m}from"../mcp/client-manager.js";import{readJsonFile as d}from"./json-file.js";import{resolveDevSpawnSpec as p}from"./dev-spawn.js";import{inferAgentSourceType as u}from"./source.js";const g=1,f="unknown",h=15e3,w=2e3,A=500;function $(t,e){return c(t,"pids",`${e}.pid`)}function k(t,e){return c(t,"pids",`${e}.runtime.json`)}function v(t,e){for(const r of[$(t,e),k(t,e)])n(r)&&s(r)}export function cleanupOrphanAgentRuntimeMetadata(t,e){return void 0===getAgentPid(t,e)&&(v(t,e),!0)}export function getAgentLogPath(t,e){return c(t,"logs",`${e}.log`)}function y(t){try{return process.kill(t,0),!0}catch{return!1}}export function getAgentPid(t,e){const r=$(t,e);if(!n(r))return;const i=Number(o(r,"utf-8").trim());if(!Number.isNaN(i)&&y(i))return i;v(t,e)}export function getRollCoreVersion(){try{const t=c(import.meta.dirname,"../../package.json"),e=d(t);return S(e)&&"string"==typeof e.version?e.version:f}catch{return f}}export function writeAgentRuntimeSidecar(t,e,i){const o=k(e,t.skill.name),s=l(o);n(s)||r(s,{recursive:!0});const c={schemaVersion:1,agentName:t.skill.name,pid:i,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};a(o,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=R(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=R(e,t.skill.name);return"invalid"===s?(o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):s?(s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):(o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o})}export async function probeAgentEndpoint(t,e={}){const n=new m;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}function x(t){const e=process.env[t];if(void 0===e||0===e.trim().length)return;const n=Number.parseInt(e,10);return!Number.isInteger(n)||n<=0||String(n)!==e.trim()?void 0:n}export async function waitForAgentReady(t,e={}){const n=x("ROLL_AGENT_READY_STARTUP_TIMEOUT_MS")??e.startupTimeoutMs??h,r=x("ROLL_AGENT_READY_PROBE_TIMEOUT_MS")??e.probeTimeoutMs??w,i=x("ROLL_AGENT_READY_INTERVAL_MS")??e.intervalMs??A,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await P(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(o,s,c){if("streamable-http"===o.transport.type&&"core-managed"!==o.runtime.ownership)throw new Error(`Agent "${o.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${o.transport.endpoint}`);const m=getAgentPid(s,o.skill.name);if(void 0!==m)throw new Error(`Agent "${o.skill.name}" 已在运行 (PID: ${String(m)})`);const d=E(o),p=getAgentLogPath(s,o.skill.name),u=l(p);n(u)||r(u,{recursive:!0});const g=i(p,"a"),f=t(d.command,[...d.args??[]],{cwd:o.installPath,detached:!0,windowsHide:!0,stdio:["ignore",g,g],...c?{env:{...process.env,...c}}:{}});if(e(g),!f.pid)throw new Error(`Failed to start agent "${o.skill.name}"`);const h=$(s,o.skill.name),w=l(h);n(w)||r(w,{recursive:!0});try{a(h,String(f.pid),"utf-8"),writeAgentRuntimeSidecar(o,s,f.pid)}catch(t){try{process.kill(f.pid,"SIGTERM")}catch{}throw v(s,o.skill.name),new Error(`Failed to persist runtime metadata for agent "${o.skill.name}"`,{cause:t})}return f.unref(),f.pid}export function stopAgent(t,e){const n=getAgentPid(t,e);if(void 0===n)return!1;try{process.kill(n,"SIGTERM")}catch{}return v(t,e),!0}export async function stopAgentGracefully(t,e,n={}){const r=getAgentPid(t,e);if(void 0===r)return!1;try{process.kill(r,"SIGTERM")}catch{return v(t,e),!0}const i=n.timeoutMs??15e3,o=n.intervalMs??200,s=Date.now()+i;for(;Date.now()<s;){if(!y(r))return v(t,e),!0;await P(o)}throw new Error(`Agent "${e}" did not stop within ${i}ms`)}function E(t){if("stdio"===t.transport.type){const e=p(t.transport.command,t.transport.args,t.installPath,u(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=p(t.runtime.start.command,t.runtime.start.args,t.installPath,u(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function P(t){return new Promise(e=>{setTimeout(e,t)})}function R(t,e){const r=k(t,e);if(!n(r))return;let i;try{i=d(r)}catch{return"invalid"}return M(i)?i:"invalid"}function M(t){return!!S(t)&&(1===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function S(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
1
+ import t from"cross-spawn";import{createHash as e,randomUUID as n}from"node:crypto";import{closeSync as r,existsSync as i,mkdirSync as o,openSync as s,readFileSync as a,statSync as c,unlinkSync as d,writeFileSync as u}from"node:fs";import{resolve as l,dirname as p}from"node:path";import{McpClientManager as m}from"../mcp/client-manager.js";import{readJsonFile as f}from"./json-file.js";import{isProcessStartToken as g,readProcessStartToken as y}from"./process-identity.js";import{resolveDevSpawnSpec as h}from"./dev-spawn.js";import{inferAgentSourceType as k}from"./source.js";const w=2,A="unknown",E=15e3,v=2e3,S=500,$=3e5,x=new WeakMap;export class AgentLifecycleBusyError extends Error{code="agent_lifecycle_busy";constructor(t){super(`Agent "${t}" 正在执行另一项生命周期操作,请稍后重试。`),this.name="AgentLifecycleBusyError"}}export class AgentRuntimeIdentityError extends Error{code="agent_runtime_identity_unverifiable";pid;constructor(t,e,n){super(`Agent "${t}" 的 runtime 身份无法安全验证 (PID: ${String(e)}):${n}。为避免停止无关进程,Roll 未发送信号且保留了 runtime 元数据。请先用系统工具确认并手动停止 PID ${String(e)},再运行 \`roll doctor --fix\` 清理过期元数据。`),this.name="AgentRuntimeIdentityError",this.pid=e}}function I(t,e){return l(t,"pids",`${e}.pid`)}function P(t,e){return l(t,"pids",`${e}.runtime.json`)}function R(t,n){const r=e("sha256").update(n).digest("hex");return l(t,"pids",`.${r}.lifecycle.lock`)}function T(t,e){for(const n of[I(t,e),P(t,e)])i(n)&&d(n)}export function cleanupOrphanAgentRuntimeMetadata(t,e){const n=acquireAgentLifecycleLock(t,e);try{return void 0===getAgentPid(t,e)&&(T(t,e),!0)}finally{n.release()}}export function getAgentLogPath(t,e){return l(t,"logs",`${e}.log`)}function M(t){try{return process.kill(t,0),!0}catch(t){return U(t,"EPERM")}}export function getAgentPid(t,e){const n=W(I(t,e));return void 0!==n&&M(n)?n:void 0}export function getRollCoreVersion(){try{const t=l(import.meta.dirname,"../../package.json"),e=f(t);return Z(e)&&"string"==typeof e.version?e.version:A}catch{return A}}export function writeAgentRuntimeSidecar(t,e,n){const r=y(n);if(void 0===r)throw new AgentRuntimeIdentityError(t.skill.name,n,"无法读取 OS 进程启动身份");const s=P(e,t.skill.name),a=p(s);i(a)||o(a,{recursive:!0});const c={schemaVersion:2,agentName:t.skill.name,pid:n,processStartToken:r,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};u(s,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=K(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=K(e,t.skill.name);if("invalid"===s)return o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:D(t.skill.name,n)}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o};if(!s)return o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:D(t.skill.name,n)}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o};if(s.agentName!==t.skill.name&&o.push({code:"agent-name-mismatch",message:`runtime sidecar Agent 名称 ${s.agentName} 与当前 Agent ${t.skill.name} 不一致`,fix:D(t.skill.name,n)}),s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:D(t.skill.name,n)}),s.pid===n){const e=y(n);void 0===e?o.push({code:"process-identity-unavailable",message:`无法读取 PID ${String(n)} 的 OS 进程启动身份`,fix:D(t.skill.name,n)}):e!==s.processStartToken&&o.push({code:"process-identity-mismatch",message:`PID ${String(n)} 当前属于另一个进程实例,runtime 元数据已过期`,fix:D(t.skill.name,n)})}return s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}}function D(t,e){return`先用系统工具确认并手动停止 PID ${String(e)},再运行 \`roll doctor --fix\` 清理 ${t} 的过期 runtime 元数据`}export async function probeAgentEndpoint(t,e={}){const n=new m;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}function N(t){const e=process.env[t];if(void 0===e||0===e.trim().length)return;const n=Number.parseInt(e,10);return!Number.isInteger(n)||n<=0||String(n)!==e.trim()?void 0:n}export async function waitForAgentReady(t,e={}){const n=N("ROLL_AGENT_READY_STARTUP_TIMEOUT_MS")??e.startupTimeoutMs??E,r=N("ROLL_AGENT_READY_PROBE_TIMEOUT_MS")??e.probeTimeoutMs??v,i=N("ROLL_AGENT_READY_INTERVAL_MS")??e.intervalMs??S,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await z(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(t,e,n,r={}){const i=j(e,t.skill.name,r.lifecycleLock);try{return L(t,e,n)}finally{i.acquired&&i.lock.release()}}function L(e,n,a){if("streamable-http"===e.transport.type&&"core-managed"!==e.runtime.ownership)throw new Error(`Agent "${e.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${e.transport.endpoint}`);const c=getAgentPid(n,e.skill.name);if(void 0!==c)throw new Error(`Agent "${e.skill.name}" 已在运行 (PID: ${String(c)})`);const d=X(e),l=getAgentLogPath(n,e.skill.name),m=p(l);i(m)||o(m,{recursive:!0});const f=s(l,"a"),g=t(d.command,[...d.args??[]],{cwd:e.installPath,detached:!0,windowsHide:!0,stdio:["ignore",f,f],...a?{env:{...process.env,...a}}:{}});if(r(f),!g.pid)throw new Error(`Failed to start agent "${e.skill.name}"`);const y=I(n,e.skill.name),h=p(y);i(h)||o(h,{recursive:!0});try{u(y,String(g.pid),"utf-8"),writeAgentRuntimeSidecar(e,n,g.pid)}catch(t){try{g.kill("SIGTERM")}catch{}throw T(n,e.skill.name),new Error(`Failed to persist runtime metadata for agent "${e.skill.name}"`,{cause:t})}return g.unref(),g.pid}export function stopAgent(t,e){const n=acquireAgentLifecycleLock(t,e);try{return b(t,e)}finally{n.release()}}function b(t,e){const n=getAgentPid(t,e);if(void 0===n)return T(t,e),!1;const r=O(t,e,n);try{V(e,r)}catch(t){if(!U(t,"ESRCH"))throw t}return Y(t,e,r),!0}export async function stopAgentGracefully(t,e,n={}){const r=j(t,e,n.lifecycleLock);try{return await _(t,e,n)}finally{r.acquired&&r.lock.release()}}async function _(t,e,n){const r=getAgentPid(t,e);if(void 0===r)return T(t,e),!1;const i=O(t,e,r);if(void 0!==n.expectedIdentity&&!C(i,n.expectedIdentity))return!1;try{V(e,i)}catch(n){if(!U(n,"ESRCH"))throw n;return Y(t,e,i),!0}const o=n.timeoutMs??15e3,s=n.intervalMs??200,a=Date.now()+o;for(;Date.now()<a;){if(!M(r))return Y(t,e,i),!0;const n=y(r);if(void 0!==n&&n!==i.processStartToken)return Y(t,e,i),!0;await z(s)}throw new Error(`Agent "${e}" did not stop within ${o}ms`)}function O(t,e,n){const r=K(t,e);if(void 0===r)throw new AgentRuntimeIdentityError(e,n,"缺少 runtime sidecar");if("invalid"===r)throw new AgentRuntimeIdentityError(e,n,"runtime sidecar 无效或来自不含 processStartToken 的旧版本");if(r.agentName!==e||r.pid!==n)throw new AgentRuntimeIdentityError(e,n,"runtime sidecar 与当前 Agent/PID 不一致");const i=y(n);if(void 0===i)throw new AgentRuntimeIdentityError(e,n,"无法读取 OS 进程启动身份");if(i!==r.processStartToken)throw new AgentRuntimeIdentityError(e,n,"PID 当前属于另一个进程实例");return{pid:n,processStartToken:r.processStartToken,startedAt:r.startedAt}}function V(t,e){const n=y(e.pid);if(void 0===n)throw new AgentRuntimeIdentityError(t,e.pid,"发送信号前无法重新验证 OS 身份");if(n!==e.processStartToken)throw new AgentRuntimeIdentityError(t,e.pid,"发送信号前 PID 已被其他进程复用");process.kill(e.pid,"SIGTERM")}function C(t,e){return t.pid===e.pid&&t.processStartToken===e.processStartToken&&t.startedAt===e.startedAt}export function acquireAgentLifecycleLock(t,e){const r=l(t),i=R(r,e);o(p(i),{recursive:!0});const s=n(),a=y(process.pid);if(void 0===a)throw new Error(`无法验证当前 Roll 进程 (PID: ${String(process.pid)}) 的 OS 启动身份,拒绝获取 Agent lifecycle lock。`);const c={pid:process.pid,processStartToken:a,token:s,createdAtMs:Date.now()};for(let t=0;t<2;t+=1)try{u(i,`${JSON.stringify(c)}\n`,{encoding:"utf-8",flag:"wx",mode:384});const t={release:()=>q(t)};return x.set(t,{dataDir:r,agentName:e,lockPath:i,token:s,released:!1}),t}catch(n){if(!U(n,"EEXIST"))throw n;if(0===t&&G(i))continue;throw new AgentLifecycleBusyError(e)}throw new AgentLifecycleBusyError(e)}function j(t,e,n){if(void 0===n)return{lock:acquireAgentLifecycleLock(t,e),acquired:!0};const r=x.get(n);if(void 0===r||r.released||r.dataDir!==l(t)||r.agentName!==e)throw new Error("Invalid Agent lifecycle lock handle.");return{lock:n,acquired:!1}}function q(t){const e=x.get(t);if(void 0!==e&&!e.released){e.released=!0,x.delete(t);try{const t=B(e.lockPath);t?.token===e.token&&d(e.lockPath)}catch{}}}function G(t){let e;try{e=a(t,"utf-8")}catch{return!1}const n=F(e);let r;try{r=Date.now()-(n?.createdAtMs??c(t).mtimeMs)}catch{return!1}if(!(void 0===n?r>$:H(n)))return!1;try{return a(t,"utf-8")===e&&(d(t),!0)}catch{return!1}}function B(t){if(i(t))return F(a(t,"utf-8"))}function F(t){let e;try{e=JSON.parse(t)}catch{return}if(Z(e)&&"number"==typeof e.pid&&Number.isInteger(e.pid)&&!(e.pid<=0)&&g(e.processStartToken)&&"string"==typeof e.token&&"number"==typeof e.createdAtMs&&Number.isFinite(e.createdAtMs))return{pid:e.pid,processStartToken:e.processStartToken,token:e.token,createdAtMs:e.createdAtMs}}function H(t){if(!J(t.pid))return!0;const e=y(t.pid);return void 0!==e&&e!==t.processStartToken}function J(t){try{return process.kill(t,0),!0}catch(t){return U(t,"EPERM")}}function U(t,e){return t instanceof Error&&"code"in t&&t.code===e}function Y(t,e,n){const r=I(t,e);if(W(r)!==n.pid)return!1;const i=P(t,e),o=K(t,e);if(void 0===o||"invalid"===o||o.agentName!==e||!C(o,n))return!1;try{return d(r),d(i),!0}catch(t){if(U(t,"ENOENT"))return!1;throw t}}function W(t){try{const e=Number(a(t,"utf-8").trim());return Number.isInteger(e)&&e>0?e:void 0}catch{return}}function X(t){if("stdio"===t.transport.type){const e=h(t.transport.command,t.transport.args,t.installPath,k(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=h(t.runtime.start.command,t.runtime.start.args,t.installPath,k(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function z(t){return new Promise(e=>{setTimeout(e,t)})}function K(t,e){const n=P(t,e);if(!i(n))return;let r;try{r=f(n)}catch{return"invalid"}return Q(r)?r:"invalid"}function Q(t){return!!Z(t)&&(2===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||!g(t.processStartToken)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function Z(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
@@ -1 +1 @@
1
- import{writeFileSync as t,existsSync as e,mkdirSync as n,renameSync as r}from"node:fs";import{resolve as i,dirname as s}from"node:path";import{readJsonFile as a}from"./json-file.js";import{inferAgentSourceFromInstallPath as o}from"./source.js";import{AGENT_STATUSES as u,AGENT_STORE_SCHEMA_VERSION as c,createDefaultRuntimeForTransport as l}from"../types/agent.js";const p="agents.json";export class AgentStore{storePath;constructor(t){this.storePath=i(t,p)}list(){return this.load().agents}findByName(t){return this.list().find(e=>e.skill.name===t)}add(t){const e=[...this.list()];if(-1!==e.findIndex(e=>e.skill.name===t.skill.name))throw new Error(`Agent "${t.skill.name}" is already registered`);e.push(t),this.save(e)}remove(t){const e=this.list(),n=e.filter(e=>e.skill.name!==t);return n.length!==e.length&&(this.save([...n]),!0)}replace(t,e){const n=[...this.list()],r=n.findIndex(e=>e.skill.name===t);if(-1===r)return!1;if(-1!==n.findIndex((t,n)=>n!==r&&t.skill.name===e.skill.name))throw new Error(`Agent "${e.skill.name}" is already registered`);return n[r]=e,this.save(n),!0}updateStatus(t,e){const n=this.list().map(n=>n.skill.name===t?{...n,status:e}:n);this.save([...n])}save(i){const a=s(this.storePath);e(a)||n(a,{recursive:!0});const o={schemaVersion:c,agents:i},u=`${this.storePath}.tmp`;t(u,JSON.stringify(o,null,2),"utf-8"),r(u,this.storePath)}load(){if(!e(this.storePath))return f();let t;try{t=a(this.storePath)}catch{return f()}return d(t)}}function f(){return{schemaVersion:c,agents:[]}}function d(t){if(Array.isArray(t))return{schemaVersion:c,agents:t.flatMap(t=>{const e=m(t);return e?[e]:[]})};if(!D(t))return f();const e=t.agents;return Array.isArray(e)?{schemaVersion:t.schemaVersion===c?c:2,agents:e.flatMap(t=>{const e=m(t);return e?[e]:[]})}:f()}function m(t){if(!D(t))return;const e=h(t.skill),n=v(t.transport),r=N(t.installPath);if(!e||!n||!r)return;const i=w(t.runtime,n),s=P(t.source,r,n),a=N(t.registeredAt)??new Date(0).toISOString(),o=j(t.status),u=N(t.skillBody);return{skill:e,transport:n,runtime:i,installPath:r,registeredAt:a,status:o,...s?{source:s}:{},...u?{skillBody:u}:{}}}function h(t){if(!D(t))return;const e=N(t.name),n=N(t.description);if(!e||!n)return;const r=N(t.license),i=N(t.compatibility),s=y(t.env);return{name:e,description:n,...r?{license:r}:{},...i?{compatibility:i}:{},metadata:g(t.metadata),...s?{env:s}:{}}}function g(t){if(!D(t))return{};const e={};for(const[n,r]of Object.entries(t))e[n]=String(r);return e}function y(t){if(!D(t))return;const e=k(t.required),n=k(t.optional);return e||n?{...e?{required:e}:{},...n?{optional:n}:{}}:void 0}function k(t){if(!Array.isArray(t))return;const e=t.flatMap(t=>{if(!D(t))return[];const e=N(t.name);if(!e)return[];const n=N(t.purpose),r=N(t.example),i=N(t.default);return[{name:e,...n?{purpose:n}:{},...r?{example:r}:{},...i?{default:i}:{}}]});return e.length>0?e:void 0}function v(t){if(!D(t))return;const e=N(t.type);if("stdio"===e){const n=N(t.command);if(!n)return;const r=x(t.args);return r?{type:e,command:n,args:r}:{type:e,command:n}}if("streamable-http"===e){const n=N(t.endpoint);if(!n)return;return{type:e,endpoint:n}}}function w(t,e){if(!D(t))return l(e);const n=N(t.ownership);if("on-demand"===n)return{ownership:n};if("external-managed"===n)return{ownership:n};if("core-managed"===n&&"streamable-http"===e.type){const e=A(t.start),r=b(t.endpoint);if(e&&r){const i=S(t.setup);return i?{ownership:n,start:e,endpoint:r,setup:i}:{ownership:n,start:e,endpoint:r}}}return l(e)}function A(t){if(!D(t))return;const e=N(t.command);if(!e)return;const n=x(t.args);return n?{command:e,args:n}:{command:e}}function b(t){if(!D(t))return;const e=N(t.path),n="number"==typeof t.port?t.port:void 0;return e&&void 0!==n&&Number.isInteger(n)?{path:e,port:n}:void 0}function S(t){if(!D(t))return;const e=t.playwright;if(!D(e))return;const n=x(e.browsers);return n&&0!==n.length?{playwright:{browsers:n}}:void 0}function P(t,e,n){if(!D(t))return o(e,n);const r=N(t.type);if(!r)return o(e,n);switch(r){case"git":return I(N(t.url));case"local-path":return{type:r,path:N(t.path)??e};case"installed-package":{const e=N(t.packageName),n=N(t.packageSpec),i=N(t.installDir),s=N(t.installedVersion);if(!e||!n||!i)return;return{type:r,packageName:e,packageSpec:n,installDir:i,...s?{installedVersion:s}:{}}}case"remote-manifest":{const e=N(t.endpoint)??("streamable-http"===n.type?n.endpoint:void 0);return e?{type:r,endpoint:e}:void 0}default:return V(r,t,e,n)}}function V(t,e,n,r){switch(t){case"git":return I(N(e.url));case"local":return{type:"local-path",path:N(e.path)??n};case"installed":{const t=N(e.packageName),n=N(e.packageSpec),r=N(e.installDir),i=N(e.installedVersion);if(!t||!n||!r)return;return{type:"installed-package",packageName:t,packageSpec:n,installDir:r,...i?{installedVersion:i}:{}}}case"remote":{const t=o(n,r);if(t&&"remote-manifest"!==t.type)return t;const i=N(e.endpoint)??("streamable-http"===r.type?r.endpoint:void 0);return i?{type:"remote-manifest",endpoint:i}:void 0}}}function j(t){return"string"==typeof t&&u.includes(t)?t:"idle"}function x(t){if(!Array.isArray(t))return;return t.filter(t=>"string"==typeof t)}function N(t){return"string"==typeof t&&t.length>0?t:void 0}function D(t){return"object"==typeof t&&null!==t}function I(t){return t?{type:"git",url:t}:{type:"git"}}
1
+ import{writeFileSync as t,existsSync as e,mkdirSync as n,renameSync as r}from"node:fs";import{resolve as i,dirname as s}from"node:path";import{readJsonFile as o}from"./json-file.js";import{inferAgentSourceFromInstallPath as a}from"./source.js";import{AGENT_STATUSES as u,AGENT_STORE_SCHEMA_VERSION as c,AGENT_ENV_VALUE_TYPES as l,createDefaultRuntimeForTransport as p}from"../types/agent.js";const f="agents.json";export class AgentStore{storePath;constructor(t){this.storePath=i(t,f)}list(){return this.load().agents}findByName(t){return this.list().find(e=>e.skill.name===t)}add(t){const e=[...this.list()];if(-1!==e.findIndex(e=>e.skill.name===t.skill.name))throw new Error(`Agent "${t.skill.name}" is already registered`);e.push(t),this.save(e)}remove(t){const e=this.list(),n=e.filter(e=>e.skill.name!==t);return n.length!==e.length&&(this.save([...n]),!0)}replace(t,e){const n=[...this.list()],r=n.findIndex(e=>e.skill.name===t);if(-1===r)return!1;if(-1!==n.findIndex((t,n)=>n!==r&&t.skill.name===e.skill.name))throw new Error(`Agent "${e.skill.name}" is already registered`);return n[r]=e,this.save(n),!0}updateStatus(t,e){const n=this.list().map(n=>n.skill.name===t?{...n,status:e}:n);this.save([...n])}save(i){const o=s(this.storePath);e(o)||n(o,{recursive:!0});const a={schemaVersion:c,agents:i},u=`${this.storePath}.tmp`;t(u,JSON.stringify(a,null,2),"utf-8"),r(u,this.storePath)}load(){if(!e(this.storePath))return d();let t;try{t=o(this.storePath)}catch{return d()}return m(t)}}function d(){return{schemaVersion:c,agents:[]}}function m(t){if(Array.isArray(t))return{schemaVersion:c,agents:t.flatMap(t=>{const e=h(t);return e?[e]:[]})};if(!B(t))return d();const e=t.agents;return Array.isArray(e)?{schemaVersion:t.schemaVersion===c?c:2,agents:e.flatMap(t=>{const e=h(t);return e?[e]:[]})}:d()}function h(t){if(!B(t))return;const e=g(t.skill),n=w(t.transport),r=D(t.installPath);if(!e||!n||!r)return;const i=b(t.runtime,n),s=V(t.source,r,n),o=D(t.registeredAt)??new Date(0).toISOString(),a=x(t.status),u=D(t.skillBody);return{skill:e,transport:n,runtime:i,installPath:r,registeredAt:o,status:a,...s?{source:s}:{},...u?{skillBody:u}:{}}}function g(t){if(!B(t))return;const e=D(t.name),n=D(t.description);if(!e||!n)return;const r=D(t.license),i=D(t.compatibility),s=v(t.env);return{name:e,description:n,...r?{license:r}:{},...i?{compatibility:i}:{},metadata:y(t.metadata),...s?{env:s}:{}}}function y(t){if(!B(t))return{};const e={};for(const[n,r]of Object.entries(t))e[n]=String(r);return e}function v(t){if(!B(t))return;const e=k(t.required),n=k(t.optional);return e||n?{...e?{required:e}:{},...n?{optional:n}:{}}:void 0}function k(t){if(!Array.isArray(t))return;const e=t.flatMap(t=>{if(!B(t))return[];const e=D(t.name);if(!e)return[];const n=D(t.purpose),r=D(t.example),i=D(t.default),s=D(t.type),o=l.find(t=>t===s),a=I(t.secret),u=I(t.configurable),c=N(t.sourcePath);return[{name:e,...n?{purpose:n}:{},...r?{example:r}:{},...i?{default:i}:{},...void 0!==o?{type:o}:{},...void 0!==a?{secret:a}:{},...void 0!==u?{configurable:u}:{},...void 0!==c&&c.length>0?{sourcePath:c}:{}}]});return e.length>0?e:void 0}function w(t){if(!B(t))return;const e=D(t.type);if("stdio"===e){const n=D(t.command);if(!n)return;const r=N(t.args);return r?{type:e,command:n,args:r}:{type:e,command:n}}if("streamable-http"===e){const n=D(t.endpoint);if(!n)return;return{type:e,endpoint:n}}}function b(t,e){if(!B(t))return p(e);const n=D(t.ownership);if("on-demand"===n)return{ownership:n};if("external-managed"===n)return{ownership:n};if("core-managed"===n&&"streamable-http"===e.type){const e=A(t.start),r=P(t.endpoint);if(e&&r){const i=S(t.setup);return i?{ownership:n,start:e,endpoint:r,setup:i}:{ownership:n,start:e,endpoint:r}}}return p(e)}function A(t){if(!B(t))return;const e=D(t.command);if(!e)return;const n=N(t.args);return n?{command:e,args:n}:{command:e}}function P(t){if(!B(t))return;const e=D(t.path),n="number"==typeof t.port?t.port:void 0;return e&&void 0!==n&&Number.isInteger(n)?{path:e,port:n}:void 0}function S(t){if(!B(t))return;const e=t.playwright;if(!B(e))return;const n=N(e.browsers);return n&&0!==n.length?{playwright:{browsers:n}}:void 0}function V(t,e,n){if(!B(t))return a(e,n);const r=D(t.type);if(!r)return a(e,n);switch(r){case"git":return M(D(t.url));case"local-path":return{type:r,path:D(t.path)??e};case"installed-package":{const e=D(t.packageName),n=D(t.packageSpec),i=D(t.installDir),s=D(t.installedVersion);if(!e||!n||!i)return;return{type:r,packageName:e,packageSpec:n,installDir:i,...s?{installedVersion:s}:{}}}case"remote-manifest":{const e=D(t.endpoint)??("streamable-http"===n.type?n.endpoint:void 0);return e?{type:r,endpoint:e}:void 0}default:return j(r,t,e,n)}}function j(t,e,n,r){switch(t){case"git":return M(D(e.url));case"local":return{type:"local-path",path:D(e.path)??n};case"installed":{const t=D(e.packageName),n=D(e.packageSpec),r=D(e.installDir),i=D(e.installedVersion);if(!t||!n||!r)return;return{type:"installed-package",packageName:t,packageSpec:n,installDir:r,...i?{installedVersion:i}:{}}}case"remote":{const t=a(n,r);if(t&&"remote-manifest"!==t.type)return t;const i=D(e.endpoint)??("streamable-http"===r.type?r.endpoint:void 0);return i?{type:"remote-manifest",endpoint:i}:void 0}}}function x(t){return"string"==typeof t&&u.includes(t)?t:"idle"}function N(t){if(!Array.isArray(t))return;return t.filter(t=>"string"==typeof t)}function D(t){return"string"==typeof t&&t.length>0?t:void 0}function I(t){return"boolean"==typeof t?t:void 0}function B(t){return"object"==typeof t&&null!==t}function M(t){return t?{type:"git",url:t}:{type:"git"}}
@@ -0,0 +1,27 @@
1
+ export interface SkillInvocationSummary {
2
+ readonly name: string;
3
+ readonly description: string;
4
+ readonly source: string;
5
+ }
6
+ export interface SkillInvocation {
7
+ readonly skills: readonly SkillInvocationSummary[];
8
+ readonly prompt: string;
9
+ }
10
+ export declare const SKILL_INVOCATION_PARSE_KINDS: {
11
+ readonly none: "none";
12
+ readonly valid: "valid";
13
+ readonly unknown: "unknown";
14
+ };
15
+ export type SkillInvocationParseResult = {
16
+ readonly kind: typeof SKILL_INVOCATION_PARSE_KINDS.none;
17
+ } | {
18
+ readonly kind: typeof SKILL_INVOCATION_PARSE_KINDS.valid;
19
+ readonly invocation: SkillInvocation;
20
+ } | {
21
+ readonly kind: typeof SKILL_INVOCATION_PARSE_KINDS.unknown;
22
+ readonly token: string;
23
+ };
24
+ export declare function findSkillBySlashName(token: string, skills: readonly SkillInvocationSummary[]): SkillInvocationSummary | undefined;
25
+ /** Parses one or more known leading /skill tokens without rewriting the user's request. */
26
+ export declare function parseSkillInvocationResult(input: string, skills: readonly SkillInvocationSummary[]): SkillInvocationParseResult;
27
+ export declare function parseSkillInvocation(input: string, skills: readonly SkillInvocationSummary[]): SkillInvocation | undefined;
@@ -0,0 +1 @@
1
+ export const SKILL_INVOCATION_PARSE_KINDS={none:"none",valid:"valid",unknown:"unknown"};export function findSkillBySlashName(n,o){const t=n.toLowerCase();return o.find(n=>`/${n.name}`.toLowerCase()===t)}export function parseSkillInvocationResult(n,o){let t=n.trimStart();const e=[],i=new Set;for(;;){const n=/^(\/\S+)(\s*)/.exec(t);if(!n)break;const a=findSkillBySlashName(n[1]??"",o);if(!a)return{kind:SKILL_INVOCATION_PARSE_KINDS.unknown,token:n[1]??""};const S=a.name.toLowerCase();i.has(S)||(e.push(a),i.add(S)),t=t.slice(n[0].length)}return 0===e.length?{kind:SKILL_INVOCATION_PARSE_KINDS.none}:{kind:SKILL_INVOCATION_PARSE_KINDS.valid,invocation:{skills:e,prompt:t.trim()}}}export function parseSkillInvocation(n,o){const t=parseSkillInvocationResult(n,o);return t.kind===SKILL_INVOCATION_PARSE_KINDS.valid?t.invocation:void 0}
@@ -8,11 +8,17 @@ export type AgentTransport = {
8
8
  readonly endpoint: string;
9
9
  };
10
10
  /** SKILL.md frontmatter 解析结果 */
11
+ export declare const AGENT_ENV_VALUE_TYPES: readonly ["string", "boolean", "number", "json", "url"];
12
+ export type AgentEnvValueType = (typeof AGENT_ENV_VALUE_TYPES)[number];
11
13
  export interface AgentEnvDeclaration {
12
14
  readonly name: string;
13
15
  readonly purpose?: string;
14
16
  readonly example?: string;
15
17
  readonly default?: string;
18
+ readonly type?: AgentEnvValueType;
19
+ readonly secret?: boolean;
20
+ readonly configurable?: boolean;
21
+ readonly sourcePath?: readonly string[];
16
22
  }
17
23
  export interface AgentSkillEnvDeclarations {
18
24
  readonly required?: readonly AgentEnvDeclaration[];
@@ -1 +1 @@
1
- export const AGENT_STATUSES=["idle","starting","online","error","stopped"];export const AGENT_SOURCE_TYPES=["git","local-path","installed-package","remote-manifest"];export const AGENT_RUNTIME_OWNERSHIPS=["on-demand","core-managed","external-managed"];export const AGENT_STORE_SCHEMA_VERSION=2;export function createDefaultRuntimeForTransport(e){return"stdio"===e.type?{ownership:"on-demand"}:{ownership:"external-managed"}}
1
+ export const AGENT_ENV_VALUE_TYPES=["string","boolean","number","json","url"];export const AGENT_STATUSES=["idle","starting","online","error","stopped"];export const AGENT_SOURCE_TYPES=["git","local-path","installed-package","remote-manifest"];export const AGENT_RUNTIME_OWNERSHIPS=["on-demand","core-managed","external-managed"];export const AGENT_STORE_SCHEMA_VERSION=2;export function createDefaultRuntimeForTransport(e){return"stdio"===e.type?{ownership:"on-demand"}:{ownership:"external-managed"}}
@@ -23,6 +23,9 @@ export interface ChatCompactionSummary {
23
23
  readonly kept: number;
24
24
  readonly truncatedTools?: number;
25
25
  readonly beforeInputTokens?: number;
26
+ readonly checkpointId?: string;
27
+ readonly checkpointGeneration?: number;
28
+ readonly checkpointSummaryStatus?: "valid" | "fallback" | "skipped";
26
29
  }
27
30
  export interface ChatInputRequirement {
28
31
  readonly name: string;
@@ -0,0 +1,15 @@
1
+ import type { ConfigApplicationService } from "../config/application-service.ts";
2
+ import type { RollUiController } from "./contracts.ts";
3
+ type ConfigApplicationPort = Pick<ConfigApplicationService, "read" | "previewStructured" | "previewYaml" | "saveStructured" | "saveYaml">;
4
+ export interface ConfigApplicationUiControllerOptions {
5
+ readonly config: ConfigApplicationPort;
6
+ readonly getCatalog: RollUiController["getCatalog"];
7
+ readonly getAgentStatus?: RollUiController["getAgentStatus"];
8
+ readonly applyAgentEffects?: RollUiController["applyAgentEffects"];
9
+ }
10
+ /**
11
+ * Reuses ConfigApplicationService for both structured and raw-YAML editing.
12
+ * Agent lifecycle hooks remain injectable until the lifecycle layer is wired.
13
+ */
14
+ export declare function createConfigApplicationUiController(options: ConfigApplicationUiControllerOptions): RollUiController;
15
+ export {};
@@ -0,0 +1 @@
1
+ export function createConfigApplicationUiController(n){return{getConfig:()=>n.config.read(),getCatalog:()=>n.getCatalog(),getAgentStatus:()=>n.getAgentStatus?.()??{available:!1,agents:[]},previewConfig:t=>e(n.config,t),saveConfig:e=>t(n.config,e),applyAgentEffects:e=>n.applyAgentEffects?.(e)??i(e)}}function e(e,t){return"structured"===t.mode?e.previewStructured(t.persisted,t.expectedRevision):e.previewYaml(t.yaml,t.expectedRevision)}function t(e,t){return"structured"===t.mode?e.saveStructured(t.persisted,t.expectedRevision):e.saveYaml(t.yaml,t.expectedRevision)}function i(e){return{available:!1,applied:[],skipped:e.effects,reason:"Agent lifecycle adapter is not configured."}}
@@ -0,0 +1,49 @@
1
+ import type { ConfigActivationEffect } from "../config/application-service.ts";
2
+ import type { ConfigRevision } from "../config/document-store.ts";
3
+ export declare const ROLL_UI_CONFIG_EDIT_MODES: readonly ["structured", "yaml"];
4
+ export type RollUiConfigEditMode = (typeof ROLL_UI_CONFIG_EDIT_MODES)[number];
5
+ export interface RollUiStructuredConfigRequest {
6
+ readonly mode: "structured";
7
+ readonly persisted: unknown;
8
+ readonly expectedRevision?: ConfigRevision;
9
+ }
10
+ export interface RollUiYamlConfigRequest {
11
+ readonly mode: "yaml";
12
+ readonly yaml: string;
13
+ readonly expectedRevision?: ConfigRevision;
14
+ }
15
+ export type RollUiConfigRequest = RollUiStructuredConfigRequest | RollUiYamlConfigRequest;
16
+ export type RollUiSaveConfigRequest = (Omit<RollUiStructuredConfigRequest, "expectedRevision"> & {
17
+ readonly expectedRevision: ConfigRevision;
18
+ }) | (Omit<RollUiYamlConfigRequest, "expectedRevision"> & {
19
+ readonly expectedRevision: ConfigRevision;
20
+ });
21
+ export interface RollUiApplyEffectsRequest {
22
+ /**
23
+ * Untrusted client input. The lifecycle adapter must reconcile these effects
24
+ * with the latest saved activation plan before changing process state.
25
+ */
26
+ readonly effects: readonly ConfigActivationEffect[];
27
+ }
28
+ type Awaitable<T> = T | Promise<T>;
29
+ /**
30
+ * HTTP-independent application boundary used by the local UI server.
31
+ * Implementations can be tested without opening a socket.
32
+ */
33
+ export interface RollUiController {
34
+ getConfig(): Awaitable<unknown>;
35
+ getCatalog(): Awaitable<unknown>;
36
+ getAgentStatus(): Awaitable<unknown>;
37
+ previewConfig(request: RollUiConfigRequest): Awaitable<unknown>;
38
+ saveConfig(request: RollUiSaveConfigRequest): Awaitable<unknown>;
39
+ applyAgentEffects(request: RollUiApplyEffectsRequest): Awaitable<unknown>;
40
+ }
41
+ export interface RollUiStaticAsset {
42
+ readonly body: Uint8Array | string;
43
+ readonly contentType: string;
44
+ }
45
+ /** Receives a normalized absolute URL path such as `/index.html`. */
46
+ export interface RollUiStaticAssetProvider {
47
+ getAsset(pathname: string): Awaitable<RollUiStaticAsset | null>;
48
+ }
49
+ export {};
@@ -0,0 +1 @@
1
+ export const ROLL_UI_CONFIG_EDIT_MODES=["structured","yaml"];
@@ -0,0 +1,8 @@
1
+ export { createConfigApplicationUiController } from "./config-controller.ts";
2
+ export type { ConfigApplicationUiControllerOptions } from "./config-controller.ts";
3
+ export { ROLL_UI_CONFIG_EDIT_MODES, type RollUiApplyEffectsRequest, type RollUiConfigEditMode, type RollUiConfigRequest, type RollUiController, type RollUiSaveConfigRequest, type RollUiStaticAsset, type RollUiStaticAssetProvider, type RollUiStructuredConfigRequest, type RollUiYamlConfigRequest, } from "./contracts.ts";
4
+ export { ROLL_UI_DEFAULT_BODY_LIMIT_BYTES, ROLL_UI_HOST, ROLL_UI_SESSION_COOKIE, startRollUiServer, } from "./server.ts";
5
+ export type { RollUiServerHandle, StartRollUiServerOptions } from "./server.ts";
6
+ export { createFileSystemStaticAssetProvider } from "./static-assets.ts";
7
+ export { createRollUiRuntimeController } from "./runtime-controller.ts";
8
+ export type { RollUiAgentStatusItem, RollUiAgentStatusResponse, RollUiRuntimeControllerOptions, } from "./runtime-controller.ts";
@@ -0,0 +1 @@
1
+ export{createConfigApplicationUiController}from"./config-controller.js";export{ROLL_UI_CONFIG_EDIT_MODES}from"./contracts.js";export{ROLL_UI_DEFAULT_BODY_LIMIT_BYTES,ROLL_UI_HOST,ROLL_UI_SESSION_COOKIE,startRollUiServer}from"./server.js";export{createFileSystemStaticAssetProvider}from"./static-assets.js";export{createRollUiRuntimeController}from"./runtime-controller.js";
@@ -0,0 +1,38 @@
1
+ import { type ConfigActivationEffect } from "../config/application-service.ts";
2
+ import type { RollConfig } from "../config/schema.ts";
3
+ import { type AgentActivationResult, type AgentLifecycleBaseline, type AgentLifecycleInspection } from "../registry/agent-lifecycle.ts";
4
+ import type { RegisteredAgent } from "../types/agent.ts";
5
+ import type { RollUiController } from "./contracts.ts";
6
+ interface AgentLifecyclePort {
7
+ inspectAll(): Promise<readonly AgentLifecycleInspection[]>;
8
+ captureBaseline(): AgentLifecycleBaseline;
9
+ applyActivation(effects: readonly ConfigActivationEffect[], baseline: AgentLifecycleBaseline, effectiveConfig: RollConfig): Promise<AgentActivationResult>;
10
+ }
11
+ export interface RollUiRuntimeControllerOptions {
12
+ readonly configPath: string;
13
+ readonly createLifecycle?: (dataDir: string) => AgentLifecyclePort;
14
+ readonly now?: () => Date;
15
+ }
16
+ export declare class RollUiActivationInProgressError extends Error {
17
+ readonly code: "activation_in_progress";
18
+ constructor();
19
+ }
20
+ export interface RollUiAgentStatusItem {
21
+ readonly name: string;
22
+ readonly ownership: RegisteredAgent["runtime"]["ownership"];
23
+ readonly status: AgentLifecycleInspection["state"];
24
+ readonly healthy: boolean;
25
+ readonly pid?: number;
26
+ readonly endpoint?: string;
27
+ readonly detail: string;
28
+ }
29
+ export interface RollUiAgentStatusResponse {
30
+ readonly agents: readonly RollUiAgentStatusItem[];
31
+ readonly checkedAt: string;
32
+ }
33
+ /**
34
+ * Final UI application composition: config catalog, safe save, trusted activation plan,
35
+ * and process status all share the same on-disk configuration and Agent registry.
36
+ */
37
+ export declare function createRollUiRuntimeController(options: RollUiRuntimeControllerOptions): RollUiController;
38
+ export {};
@@ -0,0 +1 @@
1
+ import{isDeepStrictEqual as e}from"node:util";import{existsSync as t}from"node:fs";import{ConfigApplicationService as n,ConfigApplicationValidationError as r}from"../config/application-service.js";import{buildRollConfigCatalog as a}from"../config/catalog.js";import{DEFAULT_CONFIG as i}from"../config/defaults.js";import{expandTilde as s,loadAgentsConfig as o,loadConfig as c,validateConfigText as l}from"../config/loader.js";import{AgentLifecycleService as f}from"../registry/agent-lifecycle.js";import{AgentStore as g}from"../registry/store.js";export class RollUiActivationInProgressError extends Error{code="activation_in_progress";constructor(){super("Agent 配置正在应用,请等待当前操作结束后再保存。"),this.name="RollUiActivationInProgressError"}}export function createRollUiRuntimeController(t){const r=t.createLifecycle??(e=>new f(e)),i=t.now??(()=>new Date);let s=null,o=null;const c=()=>w(t.configPath),l=()=>{const{agents:e}=c();return new n({configPath:t.configPath,agentEnvFields:h(e),redactUnknownAgentEnv:!0})},g=async()=>{const{dataDir:e}=c();return{agents:(await r(e).inspectAll()).map(A),checkedAt:i().toISOString()}};return{getConfig:()=>d(l()),getCatalog:()=>a(c().agents),getAgentStatus:g,previewConfig:e=>{const t=m(l(),e);return{...t,effects:u(t.effects,c().agents)}},saveConfig:e=>{if(null!==o)throw new RollUiActivationInProgressError;const n=l();m(n,e);const a=r(c().dataDir),i=a.captureBaseline(),f=p(n,e),g={...f,effects:u(f.effects,c().agents)};return g.changed&&(s={revision:g.snapshot.revision,effects:g.effects,baseline:i,lifecycle:a,effectiveConfig:v(t.configPath)}),g},applyAgentEffects:async t=>{const n=s;if(null===n)return{...await g(),attempted:!1,applied:!1,message:null===o?"没有等待应用的保存计划;Agent 未发生变更。":"已有配置生效操作正在执行;没有重复应用 Agent 变更。"};if(!e(t.effects,n.effects))return{...await g(),attempted:!1,applied:!1,message:"客户端生效计划与最近一次保存结果不一致;Agent 未发生变更。"};if(l().read().revision!==n.revision)return s=null,{...await g(),attempted:!1,applied:!1,message:"配置文件在保存后再次变化;为避免应用过期计划,Agent 未发生变更。"};s=null,o=n;try{const e=await n.lifecycle.applyActivation(n.effects,n.baseline,n.effectiveConfig);return{...await g(),attempted:!0,applied:e.success,result:e,message:y(e)}}finally{o===n&&(o=null)}}}}function u(e,t){const n=new Map(t.map(e=>[e.skill.name,e.runtime.ownership]));return e.map(e=>{if("restart-agent"!==e.kind||void 0===e.agentName)return e;const t=n.get(e.agentName);return"core-managed"===t?e:"on-demand"===t?{...e,kind:"next-command",title:`${e.agentName} 下次调用生效`,description:"该 Agent 为按需模式,无常驻进程;下一次 run/ask 调用会读取新配置。",requiresConfirmation:!1}:{...e,kind:"manual",title:"external-managed"===t?`${e.agentName} 需要外部重启`:`${e.agentName} 需要人工处理`,description:"external-managed"===t?"该 Agent 由外部系统管理;保存后请在外部完成重启。":"保存前的注册表中没有该 Agent;Roll UI 不会自动启动或重启它。",requiresConfirmation:!0}})}function d(e){try{return e.read()}catch(t){if(!(t instanceof r))throw t;return{...e.readForRepair(),repairMode:!0,validationIssues:t.issues}}}function m(e,t){return"structured"===t.mode?e.previewStructured(t.persisted,t.expectedRevision):e.previewYaml(t.yaml,t.expectedRevision)}function p(e,t){return"structured"===t.mode?e.saveStructured(t.persisted,t.expectedRevision):e.saveYaml(t.yaml,t.expectedRevision)}function v(e){if(t(e))return c({configPath:e}).config;const r=new n({configPath:e}).store.fallbackRaw;return l(r,e)}function w(e){let n=s(i.agents.dataDir);if(t(e))try{n=o({configPath:e}).agentsConfig.dataDir}catch{}let r=[];try{r=new g(n).list()}catch{}return{agents:r,dataDir:n}}function h(e){return e.flatMap(e=>[...e.skill.env?.required??[],...e.skill.env?.optional??[]].map(t=>({agentName:e.skill.name,name:t.name,secret:t.secret??!0})))}function A(e){const t="ready-on-demand"===e.state||"running"===e.state||"external-online"===e.state;return{name:e.agentName,ownership:e.ownership,status:e.state,healthy:t,...void 0!==e.pid?{pid:e.pid}:{},...void 0!==e.endpoint?{endpoint:e.endpoint}:{},detail:e.message}}function y(e){if(!e.success){const t=e.items.filter(e=>"failed"===e.status).map(e=>e.message);return t.length>0?`配置已保存,但 Agent 重启失败:${t.join(";")} 请检查状态后手动启动。`:"配置已保存,但 Agent 应用失败;请检查状态后手动启动。"}return e.restartedAgentNames.length>0?`配置已保存并重启:${e.restartedAgentNames.join("、")}。`:e.requiresManualAction?"配置已保存;没有自动启动已停止的 Agent,仍有人工步骤待完成。":"配置已保存;变更会在后续命令或新会话中生效。"}