openworkgraph 0.2.13 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +1 -0
- package/dist/api.js +6 -0
- package/dist/backend/adapter.d.ts +1 -0
- package/dist/backend/adapter.js +161 -8
- package/dist/backend/stdio.d.ts +1 -1
- package/dist/backend/stdio.js +2 -2
- package/dist/backend/types.d.ts +12 -0
- package/dist/builtin-skills.d.ts +56 -0
- package/dist/builtin-skills.js +514 -0
- package/dist/graph-transfer.js +7 -2
- package/dist/inputs.d.ts +1 -0
- package/dist/inputs.js +1 -1
- package/dist/operations/daemon.js +8 -2
- package/dist/operations/update.d.ts +2 -0
- package/dist/operations/update.js +30 -11
- package/dist/plugins.d.ts +4 -0
- package/dist/plugins.js +33 -1
- package/dist/runtime-backend.d.ts +3 -1
- package/dist/runtime-backend.js +11 -2
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +17 -2
- package/dist/service.d.ts +1 -0
- package/dist/service.js +28 -10
- package/dist/skill-configs.d.ts +43 -0
- package/dist/skill-configs.js +554 -0
- package/dist/skill-schema.d.ts +10 -0
- package/dist/skill-schema.js +144 -0
- package/dist/skill-sessions.d.ts +24 -0
- package/dist/skill-sessions.js +165 -0
- package/dist/skill-source.d.ts +37 -0
- package/dist/skill-source.js +104 -0
- package/dist/skills-api.d.ts +8 -4
- package/dist/skills-api.js +48 -27
- package/dist/skills-management-api.d.ts +12 -0
- package/dist/skills-management-api.js +139 -0
- package/dist/vendor/protocol/index.d.ts +4 -2
- package/dist/vendor/protocol/index.js +2 -1
- package/dist/vendor/protocol/skills.d.ts +107 -0
- package/dist/vendor/protocol/skills.js +33 -0
- package/package.json +2 -2
|
@@ -4,20 +4,39 @@ import { join, relative, isAbsolute, sep } from 'node:path';
|
|
|
4
4
|
import { SERVICE_VERSION } from "../vendor/protocol/index.js";
|
|
5
5
|
import { readInstance, requestInstance, startBackground, waitUntilStopped } from './lifecycle.js';
|
|
6
6
|
const PACKAGE_NAME = 'openworkgraph';
|
|
7
|
+
async function npmPath(args, spawnProcess) {
|
|
8
|
+
const invocation = process.platform === 'win32'
|
|
9
|
+
? { command: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', 'npm ' + args.join(' ')] }
|
|
10
|
+
: { command: 'npm', args };
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawnProcess(invocation.command, invocation.args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
13
|
+
let output = '';
|
|
14
|
+
child.stdout?.on('data', chunk => { if (output.length < 8192)
|
|
15
|
+
output += chunk.toString('utf8'); });
|
|
16
|
+
child.once('error', reject);
|
|
17
|
+
child.once('close', code => code === 0 ? resolve(output.trim()) : reject(new Error('npm path query failed')));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Recognize the package actually running from npm exec/npx's cache, including custom cache roots. */
|
|
21
|
+
export async function isNpxInstallation(cliPath, spawnProcess = spawn) {
|
|
22
|
+
try {
|
|
23
|
+
const cache = await npmPath(['config', 'get', 'cache'], spawnProcess);
|
|
24
|
+
if (!cache)
|
|
25
|
+
return false;
|
|
26
|
+
const npxRoot = await realpath(join(cache, '_npx'));
|
|
27
|
+
const runningPath = await realpath(cliPath);
|
|
28
|
+
const inside = relative(npxRoot, runningPath).split(sep);
|
|
29
|
+
return inside.length === 5 && inside[0] !== '..' && inside[0] !== '' && !isAbsolute(inside[0])
|
|
30
|
+
&& inside[1] === 'node_modules' && inside[2] === PACKAGE_NAME && inside[3] === 'dist' && inside[4] === 'cli.js';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
7
36
|
/** Compare the running package against npm's actual global package location. */
|
|
8
37
|
export async function isGlobalNpmInstallation(cliPath, spawnProcess = spawn) {
|
|
9
|
-
const invocation = process.platform === 'win32'
|
|
10
|
-
? { command: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', 'npm root -g'] }
|
|
11
|
-
: { command: 'npm', args: ['root', '-g'] };
|
|
12
38
|
try {
|
|
13
|
-
const root = await
|
|
14
|
-
const child = spawnProcess(invocation.command, invocation.args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
15
|
-
let output = '';
|
|
16
|
-
child.stdout?.on('data', chunk => { if (output.length < 8192)
|
|
17
|
-
output += chunk.toString('utf8'); });
|
|
18
|
-
child.once('error', reject);
|
|
19
|
-
child.once('close', code => code === 0 ? resolve(output.trim()) : reject(new Error('npm root failed')));
|
|
20
|
-
});
|
|
39
|
+
const root = await npmPath(['root', '-g'], spawnProcess);
|
|
21
40
|
if (!root)
|
|
22
41
|
return false;
|
|
23
42
|
const installedPath = join(root, PACKAGE_NAME);
|
package/dist/plugins.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface PluginContract {
|
|
|
34
34
|
/** Bounded pure JSON only: no getters, functions, cycles or exotic prototypes. */
|
|
35
35
|
export declare function checkedJson(value: unknown, maxBytes?: number, maxDepth?: number): Json;
|
|
36
36
|
export declare function readField(content: Json, field: string): Json | undefined;
|
|
37
|
+
/** Validate explicit references against the exact prompt occurrence, including boundaries. */
|
|
38
|
+
export declare function validSkillContent(content: Json): boolean;
|
|
39
|
+
/** Keep explicit Codex provenance while removing host paths; re-selection is required. */
|
|
40
|
+
export declare function portableSkillContent(content: Json): Json;
|
|
37
41
|
export interface PluginInformation {
|
|
38
42
|
state: 'available' | 'missing' | 'incompatible' | 'invalid';
|
|
39
43
|
typeId: string;
|
package/dist/plugins.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSkillName } from "./vendor/protocol/index.js";
|
|
1
2
|
import { ServiceError } from './errors.js';
|
|
2
3
|
const forbidden = new Set(['__proto__', 'prototype', 'constructor']);
|
|
3
4
|
const builtins = new Set(['text', 'image', 'document', 'video', 'file', 'preview', 'execution', 'group']);
|
|
@@ -154,6 +155,37 @@ function validateContract(input) {
|
|
|
154
155
|
}
|
|
155
156
|
return c;
|
|
156
157
|
}
|
|
158
|
+
/** Validate explicit references against the exact prompt occurrence, including boundaries. */
|
|
159
|
+
export function validSkillContent(content) {
|
|
160
|
+
if (!object(content) || content.skillReferences === undefined)
|
|
161
|
+
return true;
|
|
162
|
+
const refs = content.skillReferences;
|
|
163
|
+
if (!Array.isArray(refs) || refs.length > 1024 || typeof content.prompt !== 'string')
|
|
164
|
+
return false;
|
|
165
|
+
const prompt = content.prompt;
|
|
166
|
+
let end = 0;
|
|
167
|
+
for (const value of refs) {
|
|
168
|
+
if (!object(value) || Object.keys(value).some(key => !['skillId', 'source', 'name', 'start', 'end'].includes(key)))
|
|
169
|
+
return false;
|
|
170
|
+
const ref = value;
|
|
171
|
+
if (typeof ref.skillId !== 'string' || !ref.skillId.trim() || ref.skillId.length > 4096 || /[\x00-\x1f]/.test(ref.skillId) ||
|
|
172
|
+
!['openworkgraph', 'codex'].includes(ref.source) || !isSkillName(ref.name) ||
|
|
173
|
+
!Number.isSafeInteger(ref.start) || !Number.isSafeInteger(ref.end) || ref.start < end || ref.end > prompt.length ||
|
|
174
|
+
prompt.slice(ref.start, ref.end) !== '$' + ref.name ||
|
|
175
|
+
(ref.start > 0 && !/\s/.test(prompt[ref.start - 1])) || (ref.end < prompt.length && !/[\s.,!?;,。!?;]/.test(prompt[ref.end])))
|
|
176
|
+
return false;
|
|
177
|
+
end = ref.end;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
/** Keep explicit Codex provenance while removing host paths; re-selection is required. */
|
|
182
|
+
export function portableSkillContent(content) {
|
|
183
|
+
if (!object(content) || content.skillReferences === undefined)
|
|
184
|
+
return content;
|
|
185
|
+
if (!validSkillContent(content))
|
|
186
|
+
invalid('Invalid skill references');
|
|
187
|
+
return { ...content, skillReferences: content.skillReferences.map(ref => object(ref) && ref.source === 'codex' ? { ...ref, skillId: 'codex-unresolved:' + ref.name } : ref) };
|
|
188
|
+
}
|
|
157
189
|
export class PluginRegistry {
|
|
158
190
|
db;
|
|
159
191
|
constructor(db) {
|
|
@@ -209,7 +241,7 @@ export class PluginRegistry {
|
|
|
209
241
|
}
|
|
210
242
|
if (!contract)
|
|
211
243
|
return { state: 'missing', typeId: type, schemaVersion, content, reason: 'Resource contract is unavailable', contract: null };
|
|
212
|
-
const valid = matches(contract.contentSchema, content);
|
|
244
|
+
const valid = matches(contract.contentSchema, content) && (!builtins.has(type) || validSkillContent(content));
|
|
213
245
|
return { state: valid ? 'available' : 'invalid', typeId: type, schemaVersion, content, reason: valid ? null : 'Content does not match schema', contract };
|
|
214
246
|
}
|
|
215
247
|
/** Pure copy-on-success migration. Caller persists new node version atomically; old version is never changed. */
|
|
@@ -5,6 +5,7 @@ import { Runs } from './runs.js';
|
|
|
5
5
|
import { ProjectProbe } from './project-probe.js';
|
|
6
6
|
import { type DataDirectories } from './directories.js';
|
|
7
7
|
import { RunContexts } from './run-context.js';
|
|
8
|
+
import type { SkillSessions } from './skill-sessions.js';
|
|
8
9
|
export declare function backendReply(interaction: Interaction, answer: Json): BackendReply;
|
|
9
10
|
/** Trusted host bridge. Never accepts browser paths or creates isolation evidence. */
|
|
10
11
|
export declare class RuntimeBackend implements SchedulerBackend {
|
|
@@ -12,9 +13,10 @@ export declare class RuntimeBackend implements SchedulerBackend {
|
|
|
12
13
|
readonly dirs: DataDirectories;
|
|
13
14
|
readonly adapter: BackendAdapter;
|
|
14
15
|
readonly probe: ProjectProbe;
|
|
16
|
+
readonly skillSessions?: SkillSessions | undefined;
|
|
15
17
|
private readonly contexts;
|
|
16
18
|
readonly runContexts: RunContexts;
|
|
17
|
-
constructor(runs: Runs, dirs: DataDirectories, adapter: BackendAdapter, probe?: ProjectProbe);
|
|
19
|
+
constructor(runs: Runs, dirs: DataDirectories, adapter: BackendAdapter, probe?: ProjectProbe, skillSessions?: SkillSessions | undefined);
|
|
18
20
|
private snapshot;
|
|
19
21
|
start(run: Run, snapshot: InputSnapshot, context: BackendContext): Promise<void>;
|
|
20
22
|
queryHistory(runId: string, query: string): string;
|
package/dist/runtime-backend.js
CHANGED
|
@@ -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.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function createService(dataDir?: string, options?: {
|
|
|
11
11
|
projectProbe?: ProjectProbe;
|
|
12
12
|
runtime?: WorkflowRuntimeOptions;
|
|
13
13
|
npmInstallation?: () => Promise<boolean>;
|
|
14
|
+
npxInstallation?: () => Promise<boolean>;
|
|
14
15
|
latestNpmVersion?: () => Promise<string>;
|
|
15
16
|
updateRuntime?: (onExit: () => void) => Promise<void>;
|
|
16
17
|
}): {
|
package/dist/service.js
CHANGED
|
@@ -58,6 +58,7 @@ export function createService(dataDir, options = {}) {
|
|
|
58
58
|
const events = new Events(db, serviceId, auth);
|
|
59
59
|
const instanceId = randomUUID();
|
|
60
60
|
const npmInstallation = options.npmInstallation ?? (async () => false);
|
|
61
|
+
const npxInstallation = options.npxInstallation ?? (async () => false);
|
|
61
62
|
let updateQueued = false;
|
|
62
63
|
const unavailable = {
|
|
63
64
|
status: "unavailable",
|
|
@@ -136,14 +137,15 @@ export function createService(dataDir, options = {}) {
|
|
|
136
137
|
const rawPath = request.url ?? "";
|
|
137
138
|
const fileRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]files(?:[/](?:search|content|link-content|media|thumbnail|stat))?(?:[?]|$)/.test(rawPath);
|
|
138
139
|
const skillsRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]skills[/]search(?:[?]|$)/.test(rawPath);
|
|
140
|
+
const skillsManagementRoute = /^[/]v1[/]skills(?:[/][^/?#]+(?:[/](?:install|update|uninstall|config|files))?)?(?:[?]|$)/.test(rawPath);
|
|
139
141
|
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;
|
|
142
|
+
const queryRoute = fileRoute || skillsRoute || skillsManagementRoute || mediaRoute;
|
|
141
143
|
const path = queryRoute ? rawPath.split('?')[0] : rawPath;
|
|
142
144
|
if (!path.startsWith("/") ||
|
|
143
145
|
path.startsWith("//") ||
|
|
144
146
|
(!queryRoute && path.includes("?")) ||
|
|
145
147
|
path.includes("#") ||
|
|
146
|
-
path.includes("%") ||
|
|
148
|
+
(!skillsManagementRoute && path.includes("%")) ||
|
|
147
149
|
path.includes("\\"))
|
|
148
150
|
throw new ServiceError("INVALID_REQUEST", "请使用标准 API 路径;凭据只能放在 Authorization 请求头,不能放入 URL。");
|
|
149
151
|
if (queryRoute && (rawPath.includes('#') || rawPath.length > 8192 || rawPath.split('?').length > 2))
|
|
@@ -170,11 +172,12 @@ export function createService(dataDir, options = {}) {
|
|
|
170
172
|
.split(",")
|
|
171
173
|
.map((value) => value.trim())
|
|
172
174
|
.filter(Boolean);
|
|
175
|
+
const allowedMethods = /^[/]v1[/]skills[/][^/]+[/]config$/.test(path) ? ["GET", "POST", "PUT", "DELETE"] : ["GET", "POST", "DELETE"];
|
|
173
176
|
if (typeof method !== "string" ||
|
|
174
|
-
!
|
|
177
|
+
!allowedMethods.includes(method) ||
|
|
175
178
|
headers.some((header) => !CORS_HEADERS.includes(header)))
|
|
176
179
|
throw new ServiceError("ORIGIN_DENIED", "不允许的跨来源方法或请求头。");
|
|
177
|
-
response.setHeader("Access-Control-Allow-Methods", "
|
|
180
|
+
response.setHeader("Access-Control-Allow-Methods", allowedMethods.join(", "));
|
|
178
181
|
response.setHeader("Access-Control-Allow-Headers", CORS_HEADERS.join(", "));
|
|
179
182
|
response.setHeader("Access-Control-Max-Age", "300");
|
|
180
183
|
response.writeHead(204);
|
|
@@ -200,7 +203,7 @@ export function createService(dataDir, options = {}) {
|
|
|
200
203
|
const runtimeInfo = runtime.serviceInfo;
|
|
201
204
|
send({
|
|
202
205
|
...info,
|
|
203
|
-
installation: await npmInstallation() ? 'npm-global' : 'other',
|
|
206
|
+
installation: await npmInstallation() ? 'npm-global' : await npxInstallation() ? 'npx' : 'other',
|
|
204
207
|
imageRoutes: runtimeInfo.imageRoutes,
|
|
205
208
|
capabilities: {
|
|
206
209
|
...info.capabilities,
|
|
@@ -288,7 +291,7 @@ export function createService(dataDir, options = {}) {
|
|
|
288
291
|
events.stream(request, response, token, origin);
|
|
289
292
|
return;
|
|
290
293
|
}
|
|
291
|
-
const business = await api.handle(request, fileRoute || skillsRoute ? rawPath : path, token, origin);
|
|
294
|
+
const business = await api.handle(request, fileRoute || skillsRoute || skillsManagementRoute ? rawPath : path, token, origin);
|
|
292
295
|
if (business.handled) {
|
|
293
296
|
const body = auth.withSession(token, origin, (current) => business.binary ?? (business.binaryParts ? undefined : events.decorate(business.body, current.id)));
|
|
294
297
|
if (business.status)
|
|
@@ -468,13 +471,29 @@ export function createService(dataDir, options = {}) {
|
|
|
468
471
|
db.prepare("INSERT INTO settings(key,value) VALUES('listener',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(listenerInfo(address, serviceId, instanceId)));
|
|
469
472
|
});
|
|
470
473
|
let closing;
|
|
474
|
+
async function closeHttpServer() {
|
|
475
|
+
if (!server.listening)
|
|
476
|
+
return;
|
|
477
|
+
await new Promise((resolve, reject) => {
|
|
478
|
+
// Runs and event streams have already drained. A browser's unfinished
|
|
479
|
+
// request must not hold the service's lifetime locks indefinitely.
|
|
480
|
+
const timeout = setTimeout(() => server.closeAllConnections(), 1000);
|
|
481
|
+
timeout.unref();
|
|
482
|
+
server.close(error => {
|
|
483
|
+
clearTimeout(timeout);
|
|
484
|
+
if (error)
|
|
485
|
+
reject(error);
|
|
486
|
+
else
|
|
487
|
+
resolve();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
471
491
|
async function abortStartup() {
|
|
472
492
|
runtime.disposeUnstarted();
|
|
473
493
|
projectProbe.close();
|
|
474
494
|
events.close();
|
|
475
495
|
try {
|
|
476
|
-
|
|
477
|
-
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
496
|
+
await closeHttpServer();
|
|
478
497
|
}
|
|
479
498
|
finally {
|
|
480
499
|
db.close();
|
|
@@ -487,8 +506,7 @@ export function createService(dataDir, options = {}) {
|
|
|
487
506
|
projectProbe.close();
|
|
488
507
|
events.close();
|
|
489
508
|
try {
|
|
490
|
-
|
|
491
|
-
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
509
|
+
await closeHttpServer();
|
|
492
510
|
db.prepare("DELETE FROM settings WHERE key='listener' AND json_extract(value,'$.instanceId')=?").run(instanceId);
|
|
493
511
|
}
|
|
494
512
|
finally {
|
|
@@ -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
|
+
}
|