@worca/app 1.3.0 → 1.4.0-rc.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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -14,17 +14,23 @@ import {
|
|
|
14
14
|
mkdirSync, rmSync, symlinkSync, renameSync, unlinkSync,
|
|
15
15
|
} from 'node:fs';
|
|
16
16
|
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
17
|
-
import { WORCA_PLUGIN_APIS } from './plugin-api.mjs';
|
|
18
|
-
import { normalizeManifest, validatePluginDir, apiSatisfies, dataContractIssues, apiMismatch } from './plugin-manifest.mjs';
|
|
17
|
+
import { WORCA_PLUGIN_APIS, WORCA_ASK_FORMS_API } from './plugin-api.mjs';
|
|
18
|
+
import { normalizeManifest, validatePluginDir, apiSatisfies, dataContractIssues, apiMismatch, negotiatedApi } from './plugin-manifest.mjs';
|
|
19
19
|
import {
|
|
20
20
|
pluginsRoot, pluginDir, pluginCurrentDir, pluginDataDir, readPluginsLock, writePluginsLock,
|
|
21
21
|
DIR_NAME_RE,
|
|
22
22
|
} from './plugins-lock.mjs';
|
|
23
23
|
import { addPluginRepo, fetchCandidate, exportVersion, repoCacheDir } from './plugin-repo.mjs';
|
|
24
24
|
import {
|
|
25
|
-
importPluginWorkflows, readPluginWorkflows, removePluginWorkflows,
|
|
25
|
+
importPluginWorkflows, readPluginWorkflows, removePluginWorkflows,
|
|
26
|
+
referencedPluginAgents, referencedPluginScripts,
|
|
26
27
|
} from './plugin-workflows.mjs';
|
|
27
28
|
import { loadAgentRegistry } from './agent-registry.mjs';
|
|
29
|
+
import { loadScriptRegistry } from './script-registry.mjs';
|
|
30
|
+
import { normalizeCases } from '../shared/graph/script-cases.mjs';
|
|
31
|
+
import { normalizeAskBlock, validateFormDef } from '../shared/forms/form-def.mjs';
|
|
32
|
+
import { fileAccepts } from '../shared/forms/answer.mjs';
|
|
33
|
+
import { normalizeScriptMeta, resolvePlatformValue } from '../shared/graph/script-meta.mjs';
|
|
28
34
|
import { pluginModelSecretStatus } from './plugin-models.mjs';
|
|
29
35
|
import { referencedPluginModels } from './config.mjs';
|
|
30
36
|
import { clearBindingsForPlugin } from './source-bindings.mjs';
|
|
@@ -62,8 +68,17 @@ function insideDir(dir, rel) {
|
|
|
62
68
|
* reviewer must see where) + requested model secrets, skills, workflows, npm
|
|
63
69
|
* dep count from the lockfile, and the exact setup commands that would run. */
|
|
64
70
|
export function buildInstallInventory(versionDir) {
|
|
65
|
-
const
|
|
71
|
+
const readManifest = readManifestAt(versionDir);
|
|
72
|
+
const manifest = readManifest
|
|
66
73
|
?? { taskSources: [], chatChannels: [], models: [], modelSecrets: [], setup: { node: false, python: null } };
|
|
74
|
+
// Ask forms are honoured only when the plugin NEGOTIATES plugin API 4: below
|
|
75
|
+
// it agent-registry.scanLayer strips the block at load and reports it as an
|
|
76
|
+
// ignored contribution. Consent describes what THIS host will do, so such
|
|
77
|
+
// forms are not promised here — `worca plugin validate` and the ignored line
|
|
78
|
+
// are what name them. An unreadable manifest negotiates null and fails
|
|
79
|
+
// CLOSED, exactly as the registry's plugin layer does (ask-forms spec §10).
|
|
80
|
+
const formsHonoured = readManifest !== null
|
|
81
|
+
&& Number(negotiatedApi(readManifest.engines?.worcaApi ?? '')) >= WORCA_ASK_FORMS_API;
|
|
67
82
|
const agents = [];
|
|
68
83
|
const aDir = join(versionDir, 'agents');
|
|
69
84
|
if (existsSync(aDir)) {
|
|
@@ -76,13 +91,55 @@ export function buildInstallInventory(versionDir) {
|
|
|
76
91
|
// unvalidated dir (the pre-install preview); an escaping agentFile falls
|
|
77
92
|
// back to the sibling and validatePluginDir refuses the install anyway.
|
|
78
93
|
let mdFile = `${key}.md`;
|
|
94
|
+
let rawMeta = null;
|
|
79
95
|
try {
|
|
80
|
-
|
|
96
|
+
rawMeta = JSON.parse(readFileSync(join(aDir, f), 'utf8'));
|
|
97
|
+
const af = rawMeta?.agentFile;
|
|
81
98
|
if (typeof af === 'string' && af.trim() && insideDir(aDir, af.trim())) mdFile = af.trim();
|
|
82
|
-
} catch { /* unreadable sidecar: fall back to the sibling */ }
|
|
99
|
+
} catch { /* unreadable sidecar: fall back to the sibling, and declare no forms */ }
|
|
83
100
|
let tools = [];
|
|
84
101
|
try { tools = parseFrontmatter(readFileSync(join(aDir, mdFile), 'utf8'))?.tools ?? []; } catch { /* md missing */ }
|
|
85
|
-
|
|
102
|
+
// Ask forms (spec §10). Consent states what the reviewer is agreeing to:
|
|
103
|
+
// how many forms this agent can put on screen, and which file types those
|
|
104
|
+
// forms may display FROM THE RUN FOLDER. Both are read from the sidecar's
|
|
105
|
+
// declared schemas — no plugin code runs here, and there is no instance
|
|
106
|
+
// data at consent time, which is why this is fileAccepts(schema) and not
|
|
107
|
+
// fileRefs(schema, data). Only forms that pass gate 1 are listed: the host
|
|
108
|
+
// drops the rest at load, and a consent card must not promise one.
|
|
109
|
+
const forms = [];
|
|
110
|
+
const accepts = new Set();
|
|
111
|
+
const declared = formsHonoured ? normalizeAskBlock(rawMeta?.ask).forms : {};
|
|
112
|
+
for (const [id, def] of Object.entries(declared)) {
|
|
113
|
+
if (!validateFormDef(def, { id }).ok) continue;
|
|
114
|
+
forms.push(id);
|
|
115
|
+
for (const a of fileAccepts(def.data)) accepts.add(a);
|
|
116
|
+
}
|
|
117
|
+
agents.push({ key, tools, forms: forms.sort(), fileTypes: [...accepts].sort() });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const scripts = [];
|
|
121
|
+
const scDir = join(versionDir, 'scripts');
|
|
122
|
+
if (existsSync(scDir)) {
|
|
123
|
+
for (const f of readdirSync(scDir).filter((x) => x.endsWith('.meta.json')).sort()) {
|
|
124
|
+
const key = f.slice(0, -'.meta.json'.length);
|
|
125
|
+
let raw = null;
|
|
126
|
+
try { raw = JSON.parse(readFileSync(join(scDir, f), 'utf8')); } catch { raw = null; }
|
|
127
|
+
const { meta } = normalizeScriptMeta(raw || {});
|
|
128
|
+
// The consent card and the receipt say how much PROOF ships with the
|
|
129
|
+
// script: a case count the same normalizer would accept (spec §8.1).
|
|
130
|
+
let cases = 0;
|
|
131
|
+
const casesFile = join(scDir, `${key}.tests.json`);
|
|
132
|
+
if (meta && existsSync(casesFile)) {
|
|
133
|
+
try { cases = normalizeCases(JSON.parse(readFileSync(casesFile, 'utf8')), meta, { shipped: true }).cases.length; }
|
|
134
|
+
catch { cases = 0; }
|
|
135
|
+
}
|
|
136
|
+
scripts.push({
|
|
137
|
+
key,
|
|
138
|
+
runtime: meta?.runtime ?? (typeof raw?.runtime === 'string' ? raw.runtime : null),
|
|
139
|
+
file: meta ? resolvePlatformValue(meta.file, process.platform) : null,
|
|
140
|
+
command: meta ? resolvePlatformValue(meta.command, process.platform) : null,
|
|
141
|
+
cases,
|
|
142
|
+
});
|
|
86
143
|
}
|
|
87
144
|
}
|
|
88
145
|
const taskSources = (manifest.taskSources || []).map((s) => ({
|
|
@@ -126,7 +183,7 @@ export function buildInstallInventory(versionDir) {
|
|
|
126
183
|
const setupCommands = [];
|
|
127
184
|
if (manifest.setup?.node) setupCommands.push(`npm ci --prefix ${versionDir} --ignore-scripts --omit=dev`);
|
|
128
185
|
if (manifest.setup?.python === 'pyproject') setupCommands.push(`uv sync --project ${versionDir}`);
|
|
129
|
-
return { agents, taskSources, chatChannels, models, modelSecrets, skills: skills.sort(), workflows, depCount, setupCommands };
|
|
186
|
+
return { agents, scripts, taskSources, chatChannels, models, modelSecrets, skills: skills.sort(), workflows, depCount, setupCommands };
|
|
130
187
|
}
|
|
131
188
|
|
|
132
189
|
/**
|
|
@@ -159,9 +216,20 @@ export function ignoredContributions(name, dir, opts = {}) {
|
|
|
159
216
|
for (const d of drops) {
|
|
160
217
|
if (d.origin === `plugin:${name}`) out.push({ file: `agents/${d.file}`, reason: d.reason });
|
|
161
218
|
}
|
|
219
|
+
// Script drops ride the same channel (script-registry's onDrop); one load unless the caller did it.
|
|
220
|
+
let scriptDrops = opts.scriptDrops;
|
|
221
|
+
let scripts = opts.scripts;
|
|
222
|
+
if (!scriptDrops) {
|
|
223
|
+
scriptDrops = [];
|
|
224
|
+
try { scripts = loadScriptRegistry({ onDrop: (d) => scriptDrops.push(d), agentKeys: Object.keys(registry || {}) }); }
|
|
225
|
+
catch { /* no resolvable home */ }
|
|
226
|
+
}
|
|
227
|
+
for (const d of scriptDrops) {
|
|
228
|
+
if (d.origin === `plugin:${name}`) out.push({ file: `scripts/${d.file}`, reason: d.reason });
|
|
229
|
+
}
|
|
162
230
|
const skips = opts.workflowSkips
|
|
163
231
|
?? (() => {
|
|
164
|
-
try { return readPluginWorkflows(name, dir, { registry, quiet: true }).skipped; }
|
|
232
|
+
try { return readPluginWorkflows(name, dir, { registry, scripts, quiet: true }).skipped; }
|
|
165
233
|
catch { return []; }
|
|
166
234
|
})();
|
|
167
235
|
for (const s of skips) out.push({ file: `workflows/${s.file}`, reason: `invalid template (${s.errors.join('; ')})` });
|
|
@@ -392,6 +460,67 @@ function gcVersions(name, keep7) {
|
|
|
392
460
|
for (const d of entries) if (!keep7.includes(d)) rmSync(join(dir, d), { recursive: true, force: true });
|
|
393
461
|
}
|
|
394
462
|
|
|
463
|
+
/** "3 scripts (node 2, python 1) · 5 cases" — the receipt's and the consent
|
|
464
|
+
* card's one-line summary of what a plugin ships (spec §8.1). Pure. */
|
|
465
|
+
export function scriptsSummary(scripts) {
|
|
466
|
+
const list = Array.isArray(scripts) ? scripts : [];
|
|
467
|
+
if (!list.length) return '';
|
|
468
|
+
const byRuntime = new Map();
|
|
469
|
+
for (const s of list) {
|
|
470
|
+
const r = s && s.runtime ? String(s.runtime) : 'unknown';
|
|
471
|
+
byRuntime.set(r, (byRuntime.get(r) || 0) + 1);
|
|
472
|
+
}
|
|
473
|
+
const runtimes = [...byRuntime.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
474
|
+
.map(([r, n]) => `${r} ${n}`).join(', ');
|
|
475
|
+
const cases = list.reduce((n, s) => n + (Number(s && s.cases) || 0), 0);
|
|
476
|
+
const head = `${list.length} script${list.length === 1 ? '' : 's'} (${runtimes})`;
|
|
477
|
+
return cases ? `${head} · ${cases} case${cases === 1 ? '' : 's'}` : head;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The notice a python-shipping plugin gets on a host without python (§8.1). */
|
|
481
|
+
export const PYTHON_MISSING_NOTICE = 'python not found';
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The python interpreter probe's verdict, or null when the probe itself is
|
|
485
|
+
* unusable. The import is DYNAMIC on purpose: this module is loaded on almost
|
|
486
|
+
* every path and the probe is needed on two (the receipt, the Plugins list).
|
|
487
|
+
* The bare `probePython({})` call is the one the probe caches for 60 s.
|
|
488
|
+
* @returns {Promise<object|null>}
|
|
489
|
+
*/
|
|
490
|
+
export async function pythonProbeOrNull() {
|
|
491
|
+
try {
|
|
492
|
+
const mod = await import('./graph/python-probe.mjs');
|
|
493
|
+
return await mod.probePython({});
|
|
494
|
+
} catch {
|
|
495
|
+
return null; // the probe itself is unusable: no notice, never a claim
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* `python not found` when this plugin ships a python script and the host has no
|
|
501
|
+
* usable interpreter. A notice, never a block (spec §8.1) — and a notice that
|
|
502
|
+
* cannot be computed degrades to nothing rather than to a claim.
|
|
503
|
+
* @param {Array<{runtime?: string}>} scripts @param {{probe?: Function}} [opts]
|
|
504
|
+
* @returns {Promise<string|null>}
|
|
505
|
+
*/
|
|
506
|
+
export async function pythonNoticeFor(scripts, { probe = pythonProbeOrNull } = {}) {
|
|
507
|
+
if (!(Array.isArray(scripts) ? scripts : []).some((s) => s && s.runtime === 'python')) return null;
|
|
508
|
+
const p = await probe();
|
|
509
|
+
return p && p.ok === false ? PYTHON_MISSING_NOTICE : null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Union the two guard lists by workflow id: ONE row per workflow with its keys
|
|
513
|
+
* merged, so a 409 payload never names the same workflow twice. */
|
|
514
|
+
function mergeReferences(agentRefs, scriptRefs) {
|
|
515
|
+
const byId = new Map();
|
|
516
|
+
for (const r of [...agentRefs, ...scriptRefs]) {
|
|
517
|
+
const prev = byId.get(r.workflowId);
|
|
518
|
+
if (prev) prev.keys = [...new Set([...prev.keys, ...r.keys])].sort();
|
|
519
|
+
else byId.set(r.workflowId, { ...r, keys: [...r.keys] });
|
|
520
|
+
}
|
|
521
|
+
return [...byId.values()];
|
|
522
|
+
}
|
|
523
|
+
|
|
395
524
|
/**
|
|
396
525
|
* Uninstall (spec §6.3). Reference guard + imported-workflow removal live in
|
|
397
526
|
* plugin-workflows.mjs: block when a non-plugin workflow still uses this
|
|
@@ -404,12 +533,20 @@ export async function uninstallPlugin(name, { purge = false } = {}) {
|
|
|
404
533
|
const lock = readPluginsLock();
|
|
405
534
|
const entry = lock[name];
|
|
406
535
|
if (!entry) throw new Error(`plugin "${name}" is not installed`);
|
|
407
|
-
|
|
408
|
-
|
|
536
|
+
// Agents AND scripts (spec §6.3, §8.2): a saved workflow that still places one
|
|
537
|
+
// of this plugin's keys blocks the removal, with the referencing list. The
|
|
538
|
+
// agents-only sentence is unchanged — it is what the 409 handler renders today.
|
|
539
|
+
const agentRefs = referencedPluginAgents(name);
|
|
540
|
+
const scriptRefs = referencedPluginScripts(name);
|
|
541
|
+
if (agentRefs.length || scriptRefs.length) {
|
|
542
|
+
const what = agentRefs.length && scriptRefs.length
|
|
543
|
+
? 'agents and scripts'
|
|
544
|
+
: (agentRefs.length ? 'agents' : 'scripts');
|
|
545
|
+
const refs = mergeReferences(agentRefs, scriptRefs);
|
|
409
546
|
throw Object.assign(
|
|
410
|
-
new Error(`plugin "${name}"
|
|
411
|
-
// Payload field is `references` — the one name
|
|
412
|
-
//
|
|
547
|
+
new Error(`plugin "${name}" ${what} are referenced by: ${refs.map((r) => r.name).join(', ')} — remove those references first`),
|
|
548
|
+
// Payload field is `references` — the one name the server's 409 handler
|
|
549
|
+
// and the CLI catch both read (code mirrors deleteAgent).
|
|
413
550
|
{ code: 'REFERENCED', references: refs },
|
|
414
551
|
);
|
|
415
552
|
}
|
|
@@ -500,6 +637,10 @@ export function listInstalledPlugins() {
|
|
|
500
637
|
let registry = null;
|
|
501
638
|
try { registry = loadAgentRegistry(undefined, { onDrop: (d) => drops.push(d) }); }
|
|
502
639
|
catch { /* no resolvable home: fall back to the file-derived view */ }
|
|
640
|
+
const scriptDrops = [];
|
|
641
|
+
let scripts = null;
|
|
642
|
+
try { scripts = loadScriptRegistry({ onDrop: (d) => scriptDrops.push(d), agentKeys: Object.keys(registry || {}) }); }
|
|
643
|
+
catch { scripts = null; }
|
|
503
644
|
return Object.keys(lock).sort().map((name) => {
|
|
504
645
|
const e = lock[name] || {};
|
|
505
646
|
const cur = pluginCurrentDir(name);
|
|
@@ -523,13 +664,18 @@ export function listInstalledPlugins() {
|
|
|
523
664
|
broken: !manifest,
|
|
524
665
|
apiMismatch: mismatch,
|
|
525
666
|
contributions: inv
|
|
526
|
-
? { agents: inv.agents.length, taskSources: inv.taskSources.length, chatChannels: inv.chatChannels.length, models: inv.models.length, skills: inv.skills.length, workflows: inv.workflows.length }
|
|
527
|
-
: { agents: 0, taskSources: 0, chatChannels: 0, models: 0, skills: 0, workflows: 0 },
|
|
667
|
+
? { agents: inv.agents.length, scripts: inv.scripts.length, taskSources: inv.taskSources.length, chatChannels: inv.chatChannels.length, models: inv.models.length, skills: inv.skills.length, workflows: inv.workflows.length }
|
|
668
|
+
: { agents: 0, scripts: 0, taskSources: 0, chatChannels: 0, models: 0, skills: 0, workflows: 0 },
|
|
669
|
+
// What the card needs to decide whether the python notice applies, without
|
|
670
|
+
// re-reading the plugin dir.
|
|
671
|
+
scriptRuntimes: inv
|
|
672
|
+
? inv.scripts.reduce((m, s) => { const r = s.runtime || 'unknown'; m[r] = (m[r] || 0) + 1; return m; }, {})
|
|
673
|
+
: {},
|
|
528
674
|
// `contributions` counts what the plugin SHIPS (files on disk); `ignored`
|
|
529
675
|
// names the ones worca refused to load, so the card can stop claiming them.
|
|
530
676
|
// A disabled plugin contributes nothing BY CHOICE — its agents are not in the
|
|
531
677
|
// registry, so its templates would misreport V4.
|
|
532
|
-
ignored: manifest && e.enabled !== false ? ignoredContributions(name, cur, { registry, drops }) : [],
|
|
678
|
+
ignored: manifest && e.enabled !== false ? ignoredContributions(name, cur, { registry, drops, scripts, scriptDrops }) : [],
|
|
533
679
|
};
|
|
534
680
|
});
|
|
535
681
|
}
|
|
@@ -11,6 +11,7 @@ import { join, basename } from 'node:path';
|
|
|
11
11
|
import { getDb, prepare, tx } from './db.mjs';
|
|
12
12
|
import { slugify } from './artifacts.mjs';
|
|
13
13
|
import { loadAgentRegistry } from './agent-registry.mjs';
|
|
14
|
+
import { loadScriptRegistry } from './script-registry.mjs';
|
|
14
15
|
import { registryPortsFn } from './graph/registry-ports.mjs';
|
|
15
16
|
import { validateGraph } from '../shared/graph/validate.mjs';
|
|
16
17
|
import { NOT_GRAPH_V2 } from './plugin-manifest.mjs';
|
|
@@ -44,7 +45,7 @@ const normDomain = (raw) => {
|
|
|
44
45
|
* callers must not re-log what the importer already logged).
|
|
45
46
|
* @returns {{ready: Array<object>, skipped: Array<{file:string, errors:string[]}>}}
|
|
46
47
|
*/
|
|
47
|
-
export function readPluginWorkflows(name, versionDir, { registry = null, quiet = false } = {}) {
|
|
48
|
+
export function readPluginWorkflows(name, versionDir, { registry = null, scripts = null, quiet = false } = {}) {
|
|
48
49
|
const origin = `plugin:${name}`;
|
|
49
50
|
const dir = join(versionDir, 'workflows');
|
|
50
51
|
let files = [];
|
|
@@ -56,7 +57,8 @@ export function readPluginWorkflows(name, versionDir, { registry = null, quiet =
|
|
|
56
57
|
// it: agent meta ports plus the universal `await` gate and the flow-card
|
|
57
58
|
// table. The templates are not in the DB yet, so resolveGraph is unavailable —
|
|
58
59
|
// registryPortsFn exists for exactly this caller and the server's save route.
|
|
59
|
-
const
|
|
60
|
+
const reg = registry || loadAgentRegistry();
|
|
61
|
+
const portsFn = registryPortsFn(reg, scripts || loadScriptRegistry({ agentKeys: Object.keys(reg) }));
|
|
60
62
|
const warn = (msg) => { if (!quiet) console.warn(msg); };
|
|
61
63
|
const skip = (f, errors) => {
|
|
62
64
|
skipped.push({ file: f, errors });
|
|
@@ -184,17 +186,17 @@ export async function removePluginWorkflows(name) {
|
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* @param {string} name
|
|
192
|
-
* @returns {
|
|
189
|
+
* The registry keys a plugin ships under `<current>/<subdir>`: the `key` field
|
|
190
|
+
* of every `*.meta.json` there. Synchronous, never throws — an already-broken
|
|
191
|
+
* install (no such dir, malformed sidecar) simply guards nothing.
|
|
192
|
+
* `<key>.tests.json` and program files are skipped by the suffix test.
|
|
193
|
+
* @param {string} name @param {string} subdir 'agents' | 'scripts'
|
|
194
|
+
* @returns {Set<string>}
|
|
193
195
|
*/
|
|
194
|
-
|
|
196
|
+
function shippedKeys(name, subdir) {
|
|
195
197
|
const keys = new Set();
|
|
196
198
|
try {
|
|
197
|
-
const dir = join(pluginCurrentDir(name),
|
|
199
|
+
const dir = join(pluginCurrentDir(name), subdir);
|
|
198
200
|
for (const f of readdirSync(dir)) {
|
|
199
201
|
if (!f.endsWith('.meta.json')) continue;
|
|
200
202
|
try {
|
|
@@ -202,13 +204,29 @@ export function referencedPluginAgents(name) {
|
|
|
202
204
|
if (typeof k === 'string' && k.trim()) keys.add(k.trim());
|
|
203
205
|
} catch { /* malformed sidecar: nothing to guard */ }
|
|
204
206
|
}
|
|
205
|
-
} catch { return
|
|
206
|
-
|
|
207
|
+
} catch { return keys; }
|
|
208
|
+
return keys;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The non-plugin workflow rows this guard must read (user rows and OTHER
|
|
212
|
+
* plugins' rows — anything not origin 'plugin:<name>'). */
|
|
213
|
+
function foreignWorkflowRows(name) {
|
|
207
214
|
getDb();
|
|
215
|
+
return prepare('SELECT id, name, steps, graph FROM workflows WHERE origin IS NULL OR origin != ?')
|
|
216
|
+
.all(`plugin:${name}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Uninstall guard input (spec §6.3): NON-plugin workflows whose steps JSON or
|
|
221
|
+
* v2 graph references one of THIS plugin's agent keys.
|
|
222
|
+
* @param {string} name
|
|
223
|
+
* @returns {Array<{workflowId: string, name: string, keys: string[]}>}
|
|
224
|
+
*/
|
|
225
|
+
export function referencedPluginAgents(name) {
|
|
226
|
+
const keys = shippedKeys(name, 'agents');
|
|
227
|
+
if (!keys.size) return [];
|
|
208
228
|
const out = [];
|
|
209
|
-
for (const row of
|
|
210
|
-
'SELECT id, name, steps, graph FROM workflows WHERE origin IS NULL OR origin != ?',
|
|
211
|
-
).all(`plugin:${name}`)) {
|
|
229
|
+
for (const row of foreignWorkflowRows(name)) {
|
|
212
230
|
const found = new Set();
|
|
213
231
|
let steps;
|
|
214
232
|
try { steps = JSON.parse(row.steps); } catch { steps = null; }
|
|
@@ -217,8 +235,8 @@ export function referencedPluginAgents(name) {
|
|
|
217
235
|
}
|
|
218
236
|
let graph;
|
|
219
237
|
try { graph = JSON.parse(row.graph || 'null'); } catch { graph = null; }
|
|
220
|
-
// v2 rows: only kind 'agent' nodes carry
|
|
221
|
-
// card never does), so the walk is narrowed rather than key-only.
|
|
238
|
+
// v2 rows: only kind 'agent' nodes carry an agent key (a task/end/and/or/
|
|
239
|
+
// combine card never does), so the walk is narrowed rather than key-only.
|
|
222
240
|
for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) {
|
|
223
241
|
if (node && node.kind === 'agent' && keys.has(node.key)) found.add(node.key);
|
|
224
242
|
}
|
|
@@ -226,3 +244,39 @@ export function referencedPluginAgents(name) {
|
|
|
226
244
|
}
|
|
227
245
|
return out;
|
|
228
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The same guard for SHIPPED SCRIPTS (spec §8.2). No `steps` walk: the v1
|
|
250
|
+
* vocabulary had no script card, so only a v2 graph can place one.
|
|
251
|
+
*
|
|
252
|
+
* A key ANOTHER owner holds on this host does not count. `worca workflow export
|
|
253
|
+
* --format plugin` bundles the user's script under its own key, so on the author's
|
|
254
|
+
* host the plugin's copy collides with the user's and is never loaded: the workflow
|
|
255
|
+
* runs the USER's script, and removing the plugin breaks nothing. Without this an
|
|
256
|
+
* author who links their own export could never remove it. A key NOBODY holds (the
|
|
257
|
+
* plugin is disabled) still guards — enabling it again is what would mend the row.
|
|
258
|
+
* @param {string} name
|
|
259
|
+
* @returns {Array<{workflowId: string, name: string, keys: string[]}>}
|
|
260
|
+
*/
|
|
261
|
+
export function referencedPluginScripts(name) {
|
|
262
|
+
const keys = shippedKeys(name, 'scripts');
|
|
263
|
+
if (!keys.size) return [];
|
|
264
|
+
let live = {};
|
|
265
|
+
try { live = loadScriptRegistry(); } catch { live = {}; }
|
|
266
|
+
for (const k of [...keys]) {
|
|
267
|
+
const held = Object.hasOwn(live, k) ? live[k] : null;
|
|
268
|
+
if (held && held.origin !== `plugin:${name}`) keys.delete(k);
|
|
269
|
+
}
|
|
270
|
+
if (!keys.size) return [];
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const row of foreignWorkflowRows(name)) {
|
|
273
|
+
const found = new Set();
|
|
274
|
+
let graph;
|
|
275
|
+
try { graph = JSON.parse(row.graph || 'null'); } catch { graph = null; }
|
|
276
|
+
for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) {
|
|
277
|
+
if (node && node.kind === 'script' && keys.has(node.key)) found.add(node.key);
|
|
278
|
+
}
|
|
279
|
+
if (found.size) out.push({ workflowId: row.id, name: row.name, keys: [...found].sort() });
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/core/policy/cache.mjs
|
|
2
|
+
// Synchronous reads of the team-policy DISCOVERY CACHE (project_config.extra.teamPolicy) for the
|
|
3
|
+
// modules that apply default-kind fields where they already read settings: config.mjs (default
|
|
4
|
+
// workflow, human in the loop, step models, the model catalog and its routing env),
|
|
5
|
+
// guardrail-store.mjs (policy guardrail sets), ask/limits.mjs (Ask Worca limits).
|
|
6
|
+
//
|
|
7
|
+
// Deliberately a LEAF: it imports only db.mjs, store.mjs and the pure registry/effective pair,
|
|
8
|
+
// never config.mjs or policy/sync.mjs, so config.mjs can import it without a cycle. It never
|
|
9
|
+
// touches git: the cache is written by policy/sync.mjs discovery (server start + hourly, or
|
|
10
|
+
// `worca policy pull`), and a project with no cache simply has no team defaults.
|
|
11
|
+
|
|
12
|
+
import { prepare, getDb } from '../db.mjs';
|
|
13
|
+
import { projectKey } from '../store.mjs';
|
|
14
|
+
import { normalizePolicyDoc } from './registry.mjs';
|
|
15
|
+
import { fieldsForRun } from './effective.mjs';
|
|
16
|
+
|
|
17
|
+
const parse = (s) => { try { const v = JSON.parse(s); return v && typeof v === 'object' ? v : null; } catch { return null; } };
|
|
18
|
+
|
|
19
|
+
/** Every cached, carrying policy on this machine: [{ slug, sha, doc, key }]. Never throws. */
|
|
20
|
+
export function cachedPolicyHomes() {
|
|
21
|
+
try {
|
|
22
|
+
getDb();
|
|
23
|
+
const rows = prepare("SELECT project_key, extra FROM project_config WHERE extra LIKE '%teamPolicy%'").all();
|
|
24
|
+
const out = []; const seen = new Set();
|
|
25
|
+
for (const r of rows) {
|
|
26
|
+
const tp = parse(r.extra)?.teamPolicy;
|
|
27
|
+
if (!tp || !tp.present || !tp.docKnown || tp.delegateTo || tp.unknownSchema || !tp.doc || !tp.slug) continue;
|
|
28
|
+
const slug = String(tp.slug).toLowerCase();
|
|
29
|
+
if (seen.has(slug)) continue;
|
|
30
|
+
seen.add(slug);
|
|
31
|
+
// Re-normalise: the cache was written by this or an older build; the reader is the contract.
|
|
32
|
+
const doc = normalizePolicyDoc(tp.doc).doc;
|
|
33
|
+
if (doc) out.push({ slug, sha: tp.headSha ?? null, doc, key: r.project_key });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
} catch { return []; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The policy that governs `projectDir` from the cache alone: its own, or the cached home it
|
|
41
|
+
* follows. null when there is none (no branch, not discovered yet, a dangling marker).
|
|
42
|
+
* @returns {{home:string, sha:string|null, doc:object}|null}
|
|
43
|
+
*/
|
|
44
|
+
export function cachedPolicyFor(projectDir) {
|
|
45
|
+
if (!projectDir) return null;
|
|
46
|
+
let key;
|
|
47
|
+
try { key = projectKey(projectDir); } catch { return null; }
|
|
48
|
+
return cachedPolicyForKey(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Same as cachedPolicyFor, by project key (Ask Worca's pinned scope carries only the key). */
|
|
52
|
+
export function cachedPolicyForKey(key) {
|
|
53
|
+
if (!key || typeof key !== 'string') return null;
|
|
54
|
+
let tp;
|
|
55
|
+
try {
|
|
56
|
+
getDb();
|
|
57
|
+
const row = prepare('SELECT extra FROM project_config WHERE project_key = ?').get(key);
|
|
58
|
+
tp = row ? parse(row.extra)?.teamPolicy : null;
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
if (!tp || !tp.present || !tp.docKnown || tp.unknownSchema) return null;
|
|
61
|
+
if (!tp.delegateTo) {
|
|
62
|
+
const doc = tp.doc ? normalizePolicyDoc(tp.doc).doc : null;
|
|
63
|
+
return doc ? { home: String(tp.slug || '').toLowerCase(), sha: tp.headSha ?? null, doc } : null;
|
|
64
|
+
}
|
|
65
|
+
const want = String(tp.delegateTo).toLowerCase();
|
|
66
|
+
const home = cachedPolicyHomes().find((h) => h.slug === want);
|
|
67
|
+
return home ? { home: home.slug, sha: home.sha, doc: home.doc } : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The team entries that apply to project runs of `projectDir`, from the cache: {} when none. */
|
|
71
|
+
export function cachedFieldsFor(projectDir) {
|
|
72
|
+
const p = cachedPolicyFor(projectDir);
|
|
73
|
+
return p ? fieldsForRun(p.doc) : {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A single default-kind team value for `projectDir` (or undefined). Soft entries are not defaults. */
|
|
77
|
+
export function teamDefault(projectDir, key) {
|
|
78
|
+
const e = cachedFieldsFor(projectDir)[key];
|
|
79
|
+
return e && e.kind === 'default' ? e.value : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The union of the model catalogs every cached home ships, first home wins on an id collision.
|
|
84
|
+
* Each entry carries `home` for the policy badge.
|
|
85
|
+
*/
|
|
86
|
+
export function policyCatalogModels() {
|
|
87
|
+
const out = []; const seen = new Set();
|
|
88
|
+
for (const h of cachedPolicyHomes()) {
|
|
89
|
+
for (const m of h.doc.catalogs?.models || []) {
|
|
90
|
+
const lc = m.id.toLowerCase();
|
|
91
|
+
if (seen.has(lc)) continue;
|
|
92
|
+
seen.add(lc);
|
|
93
|
+
out.push({ ...m, home: h.slug });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The union of the guardrail sets every cached home ships, as virtual `gp:<id>` sets. */
|
|
100
|
+
export function policyGuardrailSets() {
|
|
101
|
+
const out = []; const seen = new Set();
|
|
102
|
+
for (const h of cachedPolicyHomes()) {
|
|
103
|
+
for (const s of h.doc.catalogs?.guardrailSets || []) {
|
|
104
|
+
const id = `gp:${s.id}`;
|
|
105
|
+
if (seen.has(id.toLowerCase())) continue;
|
|
106
|
+
seen.add(id.toLowerCase());
|
|
107
|
+
out.push({
|
|
108
|
+
id, name: s.name, origin: `policy:${h.slug}`,
|
|
109
|
+
settings: { honorProjectSettings: s.honorProjectSettings !== false, envScrub: s.envScrub === true,
|
|
110
|
+
envAllowlist: [...s.envAllowlist], protectedPaths: [...s.protectedPaths], deny: [...s.deny] },
|
|
111
|
+
createdAt: '1970-01-01T00:00:00.000Z', updatedAt: '1970-01-01T00:00:00.000Z',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|