openworkgraph 0.2.12 → 0.3.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.
@@ -29,13 +29,15 @@ export class RuntimeBackend {
29
29
  dirs;
30
30
  adapter;
31
31
  probe;
32
+ skillSessions;
32
33
  contexts = new Map();
33
34
  runContexts;
34
- constructor(runs, dirs, adapter, probe = new ProjectProbe()) {
35
+ constructor(runs, dirs, adapter, probe = new ProjectProbe(), skillSessions) {
35
36
  this.runs = runs;
36
37
  this.dirs = dirs;
37
38
  this.adapter = adapter;
38
39
  this.probe = probe;
40
+ this.skillSessions = skillSessions;
39
41
  this.runContexts = new RunContexts(runs, dirs);
40
42
  }
41
43
  snapshot(snapshot, context, expectedRunId) {
@@ -83,6 +85,11 @@ export class RuntimeBackend {
83
85
  let inputFiles;
84
86
  let upstreamContext;
85
87
  try {
88
+ if (snapshot.skills?.length) {
89
+ if (!this.skillSessions)
90
+ throw new ServiceError('INPUT_BLOCKED', '冻结技能上下文不可用。');
91
+ await this.skillSessions.environment(snapshot.skills, true);
92
+ }
86
93
  if (runtime.details.kind === 'execution')
87
94
  upstreamContext = this.runContexts.lineage(run.id);
88
95
  const inspect = await this.probe.inspect([path]);
@@ -144,7 +151,7 @@ export class RuntimeBackend {
144
151
  context.emit({ type: 'stopped' });
145
152
  return;
146
153
  }
147
- const result = await this.adapter.start({ runId: run.id, kind: runtime.details.kind, projectPath: path, inputPath: directories.input, outputPath: directories.output, serviceRoot: this.dirs.root, prompt: snapshot.prompt, resources: snapshot.resources, projectFiles: snapshot.projectFiles ?? [], files: inputFiles, model: snapshot.model, ...(upstreamContext ? { upstreamContext } : {}), ...(snapshot.imageRoute?.type === 'codex' && snapshot.imageRoute.options ? { imageOptions: snapshot.imageRoute.options } : {}), sandboxMode: sandboxMode(runtime.details.sandboxMode ?? 'read-only') }, {
154
+ const result = await this.adapter.start({ runId: run.id, kind: runtime.details.kind, projectPath: path, inputPath: directories.input, outputPath: directories.output, serviceRoot: this.dirs.root, prompt: snapshot.prompt, resources: snapshot.resources, projectFiles: snapshot.projectFiles ?? [], files: inputFiles, model: snapshot.model, skills: snapshot.skills ?? [], ...(upstreamContext ? { upstreamContext } : {}), ...(snapshot.imageRoute?.type === 'codex' && snapshot.imageRoute.options ? { imageOptions: snapshot.imageRoute.options } : {}), sandboxMode: sandboxMode(runtime.details.sandboxMode ?? 'read-only') }, {
148
155
  onSnapshot: value => this.snapshot(value, context, run.id),
149
156
  onInteraction: value => {
150
157
  if (!this.runs.db.prepare('SELECT 1 FROM runs WHERE id=?').get(run.id))
@@ -160,6 +167,8 @@ export class RuntimeBackend {
160
167
  },
161
168
  queryHistory: async (query) => this.queryHistory(run.id, query),
162
169
  readRunContext: async (request) => this.runContexts.read(run.id, request),
170
+ ...(this.skillSessions ? { skillEnvironment: async (skillId) => this.skillSessions.forSkill(snapshot.skills ?? [], skillId) } : {}),
171
+ ...(this.skillSessions ? { withSkillEnvironment: (skillId, dispatch) => this.skillSessions.withEnvironment(snapshot.skills ?? [], skillId, dispatch) } : {}),
163
172
  });
164
173
  this.snapshot(result, context, run.id);
165
174
  }
package/dist/runtime.d.ts CHANGED
@@ -17,12 +17,16 @@ import type { DataDirectories } from './directories.js';
17
17
  import { ExecutionSettingsStore } from './execution-settings.js';
18
18
  import { ImageProviders } from './image-providers.js';
19
19
  import { ImageCredentials } from './image-credentials.js';
20
+ import { BuiltinSkills, type BuiltinSkillsOptions } from './builtin-skills.js';
21
+ import { SkillConfigs } from './skill-configs.js';
22
+ import { SkillSessions } from './skill-sessions.js';
20
23
  export interface WorkflowRuntimeOptions {
21
24
  backend?: SchedulerBackend;
22
25
  models?: BackendModel[];
23
26
  /** Default: discover on model reads/submission. True also probes at startup; false disables execution and discovery. */
24
27
  enableBackend?: boolean;
25
28
  backendOptions?: AdapterOptions;
29
+ skillsOptions?: BuiltinSkillsOptions;
26
30
  }
27
31
  export declare class WorkflowRuntime {
28
32
  readonly db: DatabaseSync;
@@ -44,6 +48,9 @@ export declare class WorkflowRuntime {
44
48
  readonly executionSettings: ExecutionSettingsStore;
45
49
  readonly imageProviders: ImageProviders;
46
50
  readonly imageCredentials: ImageCredentials;
51
+ readonly skills: BuiltinSkills;
52
+ readonly skillConfigs: SkillConfigs;
53
+ readonly skillSessions: SkillSessions;
47
54
  private readonly probe;
48
55
  private readonly adapter;
49
56
  private availableModels;
package/dist/runtime.js CHANGED
@@ -29,6 +29,9 @@ import { ImageProviders } from './image-providers.js';
29
29
  import { ImageCredentials } from './image-credentials.js';
30
30
  import { ApiImageAgent, RoutedImageBackend } from './api-image-agent.js';
31
31
  import { planOpenAiImage, discoverOpenAiModels } from './openai-image-driver.js';
32
+ import { BuiltinSkills } from './builtin-skills.js';
33
+ import { SkillConfigs } from './skill-configs.js';
34
+ import { SkillSessions } from './skill-sessions.js';
32
35
  class RuntimeRuns extends Runs {
33
36
  available;
34
37
  constructor(db, serviceId, dependencies, available) {
@@ -62,6 +65,9 @@ export class WorkflowRuntime {
62
65
  executionSettings;
63
66
  imageProviders;
64
67
  imageCredentials;
68
+ skills;
69
+ skillConfigs;
70
+ skillSessions;
65
71
  probe = new ProjectProbe();
66
72
  adapter;
67
73
  availableModels;
@@ -88,6 +94,9 @@ export class WorkflowRuntime {
88
94
  this.executionSettings = new ExecutionSettingsStore(db);
89
95
  this.imageCredentials = new ImageCredentials(directories, serviceId);
90
96
  this.imageProviders = new ImageProviders(db, this.imageCredentials);
97
+ this.skills = new BuiltinSkills(directories, serviceId, options.skillsOptions);
98
+ this.skillConfigs = new SkillConfigs(directories, serviceId);
99
+ this.skillSessions = new SkillSessions(this.skills, this.skillConfigs, options.backendOptions);
91
100
  this.inputs = new InputPreparation(db, { plugins: this.plugins, readRepresentation: resource => this.resources.readPreparedRepresentation(resource) });
92
101
  this.models = new ModelDefaultsStore(db);
93
102
  this.runs = new RuntimeRuns(db, serviceId, { freeze: () => { throw new ServiceError('INPUT_BLOCKED', 'Use prepareSubmission outside the auth transaction'); }, ...(!options.backend ? { validateReply: (interaction, answer) => { backendReply(interaction, answer); } } : {}) }, (_run, snapshot) => snapshot.imageRoute?.type === 'api'
@@ -97,7 +106,7 @@ export class WorkflowRuntime {
97
106
  this.publication = new Publication(db, directories, this.resources, this.graphs, this.runs);
98
107
  this.executionChains = new ExecutionChains(this.runs, this.graphs, this.resources);
99
108
  this.adapter = options.backend ? undefined : new CodexBackendAdapter(options.backendOptions);
100
- const codexBackend = options.backend ?? new RuntimeBackend(this.runs, directories, this.adapter, this.probe);
109
+ const codexBackend = options.backend ?? new RuntimeBackend(this.runs, directories, this.adapter, this.probe, this.skillSessions);
101
110
  const backend = new RoutedImageBackend(codexBackend, new ApiImageAgent(this.runs, this.resources, this.imageProviders, this.imageCredentials, directories), this.runs);
102
111
  this.scheduler = new Scheduler(this.runs, backend, { publish: (run, token) => this.publication.publish(run, token), acquireProjectLock: (_run, path) => acquireProjectLock(path) });
103
112
  }
@@ -320,6 +329,11 @@ export class WorkflowRuntime {
320
329
  }
321
330
  const scopedInputs = new InputPreparation(this.db, { plugins: this.plugins, readRepresentation: resource => prepared.get(resourceKey(resource)) ?? null });
322
331
  const scopedPreview = scopedInputs.inputPreview(request.graphId, request.nodeId, !!apiRoute);
332
+ const targetContent = this.db.prepare('SELECT v.content FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE n.id=? AND n.graph_id=?').get(request.nodeId, request.graphId);
333
+ const skillReferences = targetContent ? JSON.parse(String(targetContent.content)).skillReferences : undefined;
334
+ if (apiRoute && Array.isArray(skillReferences) && skillReferences.length)
335
+ throw new ServiceError('INPUT_BLOCKED', '独立图片 API 不支持技能引用,请切换到 Codex 或移除技能引用。');
336
+ const frozenSkills = apiRoute ? [] : await this.skillSessions.freeze(scopedPreview.prompt, skillReferences, path);
323
337
  if (apiRoute) {
324
338
  const references = [];
325
339
  for (const envelope of scopedPreview.resources) {
@@ -357,7 +371,7 @@ export class WorkflowRuntime {
357
371
  },
358
372
  freeze: () => scopedInputs.build(request.graphId, request.nodeId, apiRoute
359
373
  ? { expectedExecutionRevision: request.expectedExecutionRevision, imageRoute: apiRoute }
360
- : { expectedExecutionRevision: request.expectedExecutionRevision, model: this.models.freeze(request.modelOverride, this.availableModels), ...(request.kind === 'image_generation' ? { imageRoute: request.imageRoute?.type === 'codex' ? request.imageRoute : { type: 'codex' } } : {}) }),
374
+ : { expectedExecutionRevision: request.expectedExecutionRevision, model: this.models.freeze(request.modelOverride, this.availableModels), skills: frozenSkills, ...(request.kind === 'image_generation' ? { imageRoute: request.imageRoute?.type === 'codex' ? request.imageRoute : { type: 'codex' } } : {}) }),
361
375
  });
362
376
  return principal => scoped.submit(request, principal, request.kind === 'execution' && holdExecution);
363
377
  }
@@ -380,6 +394,7 @@ export class WorkflowRuntime {
380
394
  else {
381
395
  this.requireAvailable({ kind: token.details.kind });
382
396
  await this.refreshModels();
397
+ await this.skillSessions.environment(snapshot.skills ?? [], true);
383
398
  }
384
399
  const project = this.db.prepare('SELECT state,canonical_path FROM projects WHERE id=?').get(run.projectId);
385
400
  if (!project || project.state !== 'active')
package/dist/service.js CHANGED
@@ -136,14 +136,15 @@ export function createService(dataDir, options = {}) {
136
136
  const rawPath = request.url ?? "";
137
137
  const fileRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]files(?:[/](?:search|content|link-content|media|thumbnail|stat))?(?:[?]|$)/.test(rawPath);
138
138
  const skillsRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]skills[/]search(?:[?]|$)/.test(rawPath);
139
+ const skillsManagementRoute = /^[/]v1[/]skills(?:[/][^/?#]+(?:[/](?:install|update|uninstall|config|files))?)?(?:[?]|$)/.test(rawPath);
139
140
  const mediaRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/](?:assets[/][a-zA-Z0-9_-]+[/]versions[/][1-9][0-9]*|graphs[/][a-zA-Z0-9_-]+[/]resources[/][a-zA-Z0-9_-]+[/]versions[/][1-9][0-9]*)[/](?:thumbnail|preview|content)(?:[?]|$)/.test(rawPath);
140
- const queryRoute = fileRoute || skillsRoute || mediaRoute;
141
+ const queryRoute = fileRoute || skillsRoute || skillsManagementRoute || mediaRoute;
141
142
  const path = queryRoute ? rawPath.split('?')[0] : rawPath;
142
143
  if (!path.startsWith("/") ||
143
144
  path.startsWith("//") ||
144
145
  (!queryRoute && path.includes("?")) ||
145
146
  path.includes("#") ||
146
- path.includes("%") ||
147
+ (!skillsManagementRoute && path.includes("%")) ||
147
148
  path.includes("\\"))
148
149
  throw new ServiceError("INVALID_REQUEST", "请使用标准 API 路径;凭据只能放在 Authorization 请求头,不能放入 URL。");
149
150
  if (queryRoute && (rawPath.includes('#') || rawPath.length > 8192 || rawPath.split('?').length > 2))
@@ -170,11 +171,12 @@ export function createService(dataDir, options = {}) {
170
171
  .split(",")
171
172
  .map((value) => value.trim())
172
173
  .filter(Boolean);
174
+ const allowedMethods = /^[/]v1[/]skills[/][^/]+[/]config$/.test(path) ? ["GET", "POST", "PUT", "DELETE"] : ["GET", "POST", "DELETE"];
173
175
  if (typeof method !== "string" ||
174
- !["GET", "POST", "DELETE"].includes(method) ||
176
+ !allowedMethods.includes(method) ||
175
177
  headers.some((header) => !CORS_HEADERS.includes(header)))
176
178
  throw new ServiceError("ORIGIN_DENIED", "不允许的跨来源方法或请求头。");
177
- response.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
179
+ response.setHeader("Access-Control-Allow-Methods", allowedMethods.join(", "));
178
180
  response.setHeader("Access-Control-Allow-Headers", CORS_HEADERS.join(", "));
179
181
  response.setHeader("Access-Control-Max-Age", "300");
180
182
  response.writeHead(204);
@@ -288,7 +290,7 @@ export function createService(dataDir, options = {}) {
288
290
  events.stream(request, response, token, origin);
289
291
  return;
290
292
  }
291
- const business = await api.handle(request, fileRoute || skillsRoute ? rawPath : path, token, origin);
293
+ const business = await api.handle(request, fileRoute || skillsRoute || skillsManagementRoute ? rawPath : path, token, origin);
292
294
  if (business.handled) {
293
295
  const body = auth.withSession(token, origin, (current) => business.binary ?? (business.binaryParts ? undefined : events.decorate(business.body, current.id)));
294
296
  if (business.status)
@@ -0,0 +1,43 @@
1
+ import type { SkillConfigSchema, SkillConfiguration, SkillConfigurationWrite } from "./vendor/protocol/index.js";
2
+ import type { DataDirectories } from './directories.js';
3
+ /** Omit schema to validate an automatic skill without loading its env/defaults. */
4
+ export interface SkillConfigRevisionRequest {
5
+ skillId: string;
6
+ revision: number;
7
+ schema?: SkillConfigSchema;
8
+ }
9
+ /** Workspace-private, immutable revisions. Uninstall deliberately does not call clear. */
10
+ export declare class SkillConfigs {
11
+ private readonly directories;
12
+ private readonly serviceId;
13
+ private sid;
14
+ constructor(directories: DataDirectories, serviceId: string);
15
+ private windowsSid;
16
+ private windowsAcl;
17
+ private directory;
18
+ private locked;
19
+ private claim;
20
+ private owner;
21
+ private dead;
22
+ private recover;
23
+ private load;
24
+ private manifest;
25
+ private removeRevoked;
26
+ private snapshot;
27
+ private atomic;
28
+ private effective;
29
+ private view;
30
+ read(skillId: string, schema: SkillConfigSchema): Promise<SkillConfiguration>;
31
+ write(skillId: string, schema: SkillConfigSchema, body: SkillConfigurationWrite): Promise<SkillConfiguration>;
32
+ clear(skillId: string, expectedRevision: number): Promise<SkillConfiguration>;
33
+ revision(skillId: string): Promise<number>;
34
+ hasRevision(skillId: string, revision: number): Promise<boolean>;
35
+ environment(skillId: string, schema: SkillConfigSchema, revision: number): Promise<Record<string, string>>;
36
+ private snapshotEnvironment;
37
+ /** Deterministic multi-skill locking prevents clear between the final revision
38
+ * check, secret read, and synchronous spawn/request. No nested public store
39
+ * methods may be awaited inside dispatch. Never await a returned command. */
40
+ withRevisions<T>(requests: readonly SkillConfigRevisionRequest[], dispatch: (environments: ReadonlyMap<string, Record<string, string>>) => T): Promise<{
41
+ value: T;
42
+ }>;
43
+ }