evolcore 0.0.23 → 0.0.25
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/CHANGELOG.md +583 -516
- package/LICENSE +21 -21
- package/README.md +455 -453
- package/bin/codex-managed-acl.ps1 +160 -155
- package/bin/codex-managed-hook.mjs +209 -92
- package/bin/ec-safe-output.js +327 -245
- package/bin/ec.js +29 -29
- package/bin/install-codex-managed-hooks.mjs +555 -544
- package/dist/agents/baseagent-availability.js +4 -4
- package/dist/agents/baseagent.js +29 -1
- package/dist/agents/claude-runner.js +152 -14
- package/dist/agents/codex/app-server-client.js +18 -8
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +215 -0
- package/dist/agents/codex-runner.js +445 -63
- package/dist/agents/ecagent-runner.js +31 -16
- package/dist/agents/gemini-runner.js +82 -4
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/agentmd.js +56 -4
- package/dist/aun/aid/index.js +1 -0
- package/dist/aun/aid/managed-operation.js +80 -6
- package/dist/aun/aid/store.js +41 -0
- package/dist/aun/msg/managed-operation.js +6 -1
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +4 -5
- package/dist/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- package/dist/channels/aun.js +147 -76
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +214 -89
- package/dist/cli/agent-create.js +576 -0
- package/dist/cli/agent.js +129 -676
- package/dist/cli/aun-commands.js +419 -313
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +20 -20
- package/dist/cli/contact.js +10 -10
- package/dist/cli/ctl-command.js +22 -20
- package/dist/cli/daemon-commands.js +163 -32
- package/dist/cli/fs-command.js +67 -67
- package/dist/cli/handoff-command.js +7 -7
- package/dist/cli/index.js +79 -79
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +11 -11
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +29 -29
- package/dist/cli/response.js +30 -30
- package/dist/cli/restart-monitor.js +462 -38
- package/dist/cli/stats.js +161 -51
- package/dist/cli/trigger-command.js +56 -56
- package/dist/config/builtin-roles.js +3 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +20 -3
- package/dist/config/resolved-config-op.js +39 -1
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +72 -19
- package/dist/core/auth/operation-authorizer.js +125 -7
- package/dist/core/auth/operation-catalog.js +26 -2
- package/dist/core/auth/remote-access-store.js +3 -2
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +60 -26
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +173 -36
- package/dist/core/command/menu-catalog.js +4 -1
- package/dist/core/command/menu-handler.js +315 -75
- package/dist/core/command/menu-protocol.js +7 -2
- package/dist/core/command/slash-gate.js +1 -3
- package/dist/core/command/slash-handler.js +166 -57
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/evolagent.js +3 -3
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +85 -27
- package/dist/core/model/catalog-service.js +3 -1
- package/dist/core/model/model-catalog.js +10 -0
- package/dist/core/model/model-permission.js +4 -1
- package/dist/core/permission/approval-gateway.js +169 -33
- package/dist/core/permission/tool-policy.js +56 -10
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +327 -134
- package/dist/ipc.js +68 -21
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +11 -4
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +24 -1
- package/dist/utils/welcome.js +22 -22
- package/dist/utils/windows-autostart.js +1 -1
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +66 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +56 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +365 -364
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +71 -69
- package/kits/docs/evolcore/agent-create.md +170 -156
- package/kits/docs/evolcore/agent.md +83 -73
- package/kits/docs/evolcore/aid.md +64 -62
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +195 -195
- package/kits/docs/evolcore/contact.md +68 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +243 -243
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +152 -152
- package/kits/docs/evolcore/menu-client.md +34 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +165 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +50 -50
- package/kits/docs/evolcore/trigger.md +575 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +270 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +25 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +195 -183
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +162 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +39 -37
- package/kits/rules/04-relation.md +75 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +63 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +427 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +201 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +96 -82
- package/kits/templates/roles/member.json +97 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +86 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +38 -29
- package/kits/templates/system-fragments/channel.md +64 -64
- package/kits/templates/system-fragments/commands.md +28 -28
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +98 -94
- package/AGENTS.md +0 -62
- package/dist/utils/codex-cli.js +0 -43
- package/skills/eclink/SKILL.md +0 -84
- package/skills/eclink/agents/openai.yaml +0 -4
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import Ajv from 'ajv';
|
|
6
|
-
|
|
7
|
-
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const SCHEMA_DIR = path.resolve(SCRIPT_DIR, '..', 'schemas');
|
|
9
|
-
const BUILTIN_ROLES = ['owner', 'admin', 'member', 'visitor'];
|
|
10
|
-
const MANAGEMENT_ROLES = new Set(['owner', 'admin']);
|
|
11
|
-
|
|
12
|
-
function parseArgs(argv) {
|
|
13
|
-
const options = { apply: false, json: false, home: undefined };
|
|
14
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
15
|
-
const arg = argv[i];
|
|
16
|
-
if (arg === '--apply') options.apply = true;
|
|
17
|
-
else if (arg === '--json') options.json = true;
|
|
18
|
-
else if (arg === '--home') options.home = argv[++i];
|
|
19
|
-
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
20
|
-
else throw new Error(`Unknown argument: ${arg}`);
|
|
21
|
-
}
|
|
22
|
-
return options;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveHome(explicit) {
|
|
26
|
-
return path.resolve(explicit || process.env.EVOLCORE_HOME || path.join(os.homedir(), '.evolcore'));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function loadValidators() {
|
|
30
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
31
|
-
const load = name => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, name), 'utf8'));
|
|
32
|
-
return {
|
|
33
|
-
agent: ajv.compile(load('agent-config.schema.5.json')),
|
|
34
|
-
registry: ajv.compile(load('role-registry.schema.1.json')),
|
|
35
|
-
role: ajv.compile(load('role-config.schema.1.json')),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function readJson(file) {
|
|
40
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
41
|
-
return { value: JSON.parse(text), text };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isRecord(value) {
|
|
45
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function normalizeRoleEffortCompat(value) {
|
|
49
|
-
if (!isRecord(value) || !isRecord(value.permissions)
|
|
50
|
-
|| !Object.prototype.hasOwnProperty.call(value.permissions, 'baseagents.codex.reasoning')) return value;
|
|
51
|
-
const permissions = { ...value.permissions };
|
|
52
|
-
if (!Object.prototype.hasOwnProperty.call(permissions, 'baseagents.codex.effort')) {
|
|
53
|
-
permissions['baseagents.codex.effort'] = permissions['baseagents.codex.reasoning'];
|
|
54
|
-
}
|
|
55
|
-
delete permissions['baseagents.codex.reasoning'];
|
|
56
|
-
return { ...value, permissions };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isValidAid(name) {
|
|
60
|
-
const labels = name.split('.');
|
|
61
|
-
return labels.length >= 3 && labels.every(label => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function validateOrThrow(validate, value, file) {
|
|
65
|
-
if (validate(value)) return;
|
|
66
|
-
const detail = (validate.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
67
|
-
throw new Error(`${file}: ${detail}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function validateRoleOrThrow(validate, value, file) {
|
|
71
|
-
validateOrThrow(validate, value, file);
|
|
72
|
-
for (const [field, permission] of Object.entries(value.permissions ?? {})) {
|
|
73
|
-
if (!isRecord(permission)) continue;
|
|
74
|
-
if (Array.isArray(permission.allowedValues) && !permission.allowedValues.includes(permission.default)) {
|
|
75
|
-
throw new Error(`${file}: permissions.${field}.default must be included in allowedValues`);
|
|
76
|
-
}
|
|
77
|
-
if (Array.isArray(permission.allowedModels)
|
|
78
|
-
&& typeof permission.default === 'string'
|
|
79
|
-
&& !permission.allowedModels.some(pattern => wildcardMatch(pattern, permission.default))) {
|
|
80
|
-
throw new Error(`${file}: permissions.${field}.default must be allowed by allowedModels`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function wildcardMatch(pattern, value) {
|
|
86
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
87
|
-
return new RegExp(`^${escaped}$`).test(value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function change(file, kind, before, value) {
|
|
91
|
-
const after = `${JSON.stringify(value, null, 2)}\n`;
|
|
92
|
-
return before === after ? null : { file, kind, before, after };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function appendChange(changes, file, kind, before, value) {
|
|
96
|
-
const item = change(file, kind, before, value);
|
|
97
|
-
if (!item) return false;
|
|
98
|
-
changes.push(item);
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function planMigration(home) {
|
|
103
|
-
const validators = loadValidators();
|
|
104
|
-
const agentsRoot = path.join(home, 'agents');
|
|
105
|
-
if (!fs.existsSync(agentsRoot)) return { home, changes: [], summary: { agentsScanned: 0, agentsMigrated: 0, rolesWritten: 0 } };
|
|
106
|
-
const changes = [];
|
|
107
|
-
const summary = { agentsScanned: 0, agentsMigrated: 0, rolesWritten: 0 };
|
|
108
|
-
|
|
109
|
-
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
110
|
-
if (!entry.isDirectory() || !isValidAid(entry.name)) continue;
|
|
111
|
-
const agentFile = path.join(agentsRoot, entry.name, 'config.json');
|
|
112
|
-
if (!fs.existsSync(agentFile)) continue;
|
|
113
|
-
summary.agentsScanned += 1;
|
|
114
|
-
const { value: input, text: agentText } = readJson(agentFile);
|
|
115
|
-
if (!isRecord(input)) throw new Error(`${agentFile}: Agent config must be an object`);
|
|
116
|
-
if (typeof input.$schema_version === 'number' && input.$schema_version > 5) {
|
|
117
|
-
throw new Error(`${agentFile}: unsupported future Agent schema version ${input.$schema_version}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const rolesDir = path.join(agentsRoot, entry.name, 'roles');
|
|
121
|
-
const registryFile = path.join(rolesDir, 'index.json');
|
|
122
|
-
if (fs.existsSync(registryFile) && input.roles !== undefined) {
|
|
123
|
-
throw new Error(`${agentFile}: both config.roles and roles/index.json exist; resolve the ambiguous role source before migration`);
|
|
124
|
-
}
|
|
125
|
-
if (input.$schema_version === 5 && input.roles === undefined && fs.existsSync(registryFile)) {
|
|
126
|
-
const { value: registry } = readJson(registryFile);
|
|
127
|
-
validateOrThrow(validators.registry, registry, registryFile);
|
|
128
|
-
for (const roleId of registry.roles) {
|
|
129
|
-
const roleFile = path.join(rolesDir, `${roleId}.json`);
|
|
130
|
-
if (!fs.existsSync(roleFile)) continue;
|
|
131
|
-
const { value: roleValue, text: roleText } = readJson(roleFile);
|
|
132
|
-
const normalizedRole = normalizeRoleEffortCompat(roleValue);
|
|
133
|
-
validateRoleOrThrow(validators.role, normalizedRole, roleFile);
|
|
134
|
-
if (appendChange(changes, roleFile, 'role-config', roleText, normalizedRole)) {
|
|
135
|
-
summary.rolesWritten += 1;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const policy = isRecord(input.roles) ? input.roles : {};
|
|
142
|
-
const definitions = isRecord(policy.definitions) ? policy.definitions : {};
|
|
143
|
-
for (const roleId of Object.keys(definitions)) {
|
|
144
|
-
if (!/^[a-z0-9_-]+$/.test(roleId) || MANAGEMENT_ROLES.has(roleId)) {
|
|
145
|
-
throw new Error(`${agentFile}: invalid role id ${JSON.stringify(roleId)}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const roleIds = [...new Set([...BUILTIN_ROLES, ...Object.keys(definitions)])];
|
|
149
|
-
const defaultRoles = {
|
|
150
|
-
private: policy.defaultRoles?.private ?? null,
|
|
151
|
-
group: policy.defaultRoles?.group ?? null,
|
|
152
|
-
};
|
|
153
|
-
for (const scene of ['private', 'group']) {
|
|
154
|
-
const selected = defaultRoles[scene];
|
|
155
|
-
if (selected !== null && (MANAGEMENT_ROLES.has(selected) || !roleIds.includes(selected))) {
|
|
156
|
-
throw new Error(`${agentFile}: roles.defaultRoles.${scene} references an invalid role`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const registry = { $schema_version: 1, roles: roleIds, defaultRoles };
|
|
160
|
-
validateOrThrow(validators.registry, registry, registryFile);
|
|
161
|
-
|
|
162
|
-
const relationsDir = path.join(agentsRoot, entry.name, 'relations');
|
|
163
|
-
if (fs.existsSync(relationsDir)) {
|
|
164
|
-
const visit = dir => {
|
|
165
|
-
for (const relationEntry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
166
|
-
const full = path.join(dir, relationEntry.name);
|
|
167
|
-
if (relationEntry.isDirectory()) visit(full);
|
|
168
|
-
else if (relationEntry.isFile() && relationEntry.name === 'config.json') {
|
|
169
|
-
const relation = readJson(full).value;
|
|
170
|
-
if (relation.role !== undefined && relation.role !== null
|
|
171
|
-
&& (MANAGEMENT_ROLES.has(relation.role) || !roleIds.includes(relation.role))) {
|
|
172
|
-
throw new Error(`${full}: role references an invalid or unregistered role`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
visit(relationsDir);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const existingRegistry = fs.existsSync(registryFile) ? fs.readFileSync(registryFile, 'utf8') : null;
|
|
181
|
-
appendChange(changes, registryFile, 'role-registry', existingRegistry, registry);
|
|
182
|
-
for (const [roleId, definition] of Object.entries(definitions)) {
|
|
183
|
-
const roleFile = path.join(rolesDir, `${roleId}.json`);
|
|
184
|
-
const roleValue = normalizeRoleEffortCompat({ $schema_version: 1, ...definition });
|
|
185
|
-
validateRoleOrThrow(validators.role, roleValue, roleFile);
|
|
186
|
-
if (appendChange(changes, roleFile, 'role-config', fs.existsSync(roleFile) ? fs.readFileSync(roleFile, 'utf8') : null, roleValue)) {
|
|
187
|
-
summary.rolesWritten += 1;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const output = { ...input, $schema_version: 5 };
|
|
192
|
-
delete output.roles;
|
|
193
|
-
validateOrThrow(validators.agent, output, agentFile);
|
|
194
|
-
appendChange(changes, agentFile, 'agent', agentText, output);
|
|
195
|
-
summary.agentsMigrated += 1;
|
|
196
|
-
}
|
|
197
|
-
return { home, changes, summary };
|
|
198
|
-
}
|
|
199
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import Ajv from 'ajv';
|
|
6
|
+
|
|
7
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SCHEMA_DIR = path.resolve(SCRIPT_DIR, '..', 'schemas');
|
|
9
|
+
const BUILTIN_ROLES = ['owner', 'admin', 'member', 'visitor'];
|
|
10
|
+
const MANAGEMENT_ROLES = new Set(['owner', 'admin']);
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const options = { apply: false, json: false, home: undefined };
|
|
14
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg === '--apply') options.apply = true;
|
|
17
|
+
else if (arg === '--json') options.json = true;
|
|
18
|
+
else if (arg === '--home') options.home = argv[++i];
|
|
19
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
20
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
21
|
+
}
|
|
22
|
+
return options;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveHome(explicit) {
|
|
26
|
+
return path.resolve(explicit || process.env.EVOLCORE_HOME || path.join(os.homedir(), '.evolcore'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadValidators() {
|
|
30
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
31
|
+
const load = name => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, name), 'utf8'));
|
|
32
|
+
return {
|
|
33
|
+
agent: ajv.compile(load('agent-config.schema.5.json')),
|
|
34
|
+
registry: ajv.compile(load('role-registry.schema.1.json')),
|
|
35
|
+
role: ajv.compile(load('role-config.schema.1.json')),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(file) {
|
|
40
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
41
|
+
return { value: JSON.parse(text), text };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeRoleEffortCompat(value) {
|
|
49
|
+
if (!isRecord(value) || !isRecord(value.permissions)
|
|
50
|
+
|| !Object.prototype.hasOwnProperty.call(value.permissions, 'baseagents.codex.reasoning')) return value;
|
|
51
|
+
const permissions = { ...value.permissions };
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(permissions, 'baseagents.codex.effort')) {
|
|
53
|
+
permissions['baseagents.codex.effort'] = permissions['baseagents.codex.reasoning'];
|
|
54
|
+
}
|
|
55
|
+
delete permissions['baseagents.codex.reasoning'];
|
|
56
|
+
return { ...value, permissions };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidAid(name) {
|
|
60
|
+
const labels = name.split('.');
|
|
61
|
+
return labels.length >= 3 && labels.every(label => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateOrThrow(validate, value, file) {
|
|
65
|
+
if (validate(value)) return;
|
|
66
|
+
const detail = (validate.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
67
|
+
throw new Error(`${file}: ${detail}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateRoleOrThrow(validate, value, file) {
|
|
71
|
+
validateOrThrow(validate, value, file);
|
|
72
|
+
for (const [field, permission] of Object.entries(value.permissions ?? {})) {
|
|
73
|
+
if (!isRecord(permission)) continue;
|
|
74
|
+
if (Array.isArray(permission.allowedValues) && !permission.allowedValues.includes(permission.default)) {
|
|
75
|
+
throw new Error(`${file}: permissions.${field}.default must be included in allowedValues`);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(permission.allowedModels)
|
|
78
|
+
&& typeof permission.default === 'string'
|
|
79
|
+
&& !permission.allowedModels.some(pattern => wildcardMatch(pattern, permission.default))) {
|
|
80
|
+
throw new Error(`${file}: permissions.${field}.default must be allowed by allowedModels`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wildcardMatch(pattern, value) {
|
|
86
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
87
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function change(file, kind, before, value) {
|
|
91
|
+
const after = `${JSON.stringify(value, null, 2)}\n`;
|
|
92
|
+
return before === after ? null : { file, kind, before, after };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function appendChange(changes, file, kind, before, value) {
|
|
96
|
+
const item = change(file, kind, before, value);
|
|
97
|
+
if (!item) return false;
|
|
98
|
+
changes.push(item);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function planMigration(home) {
|
|
103
|
+
const validators = loadValidators();
|
|
104
|
+
const agentsRoot = path.join(home, 'agents');
|
|
105
|
+
if (!fs.existsSync(agentsRoot)) return { home, changes: [], summary: { agentsScanned: 0, agentsMigrated: 0, rolesWritten: 0 } };
|
|
106
|
+
const changes = [];
|
|
107
|
+
const summary = { agentsScanned: 0, agentsMigrated: 0, rolesWritten: 0 };
|
|
108
|
+
|
|
109
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
110
|
+
if (!entry.isDirectory() || !isValidAid(entry.name)) continue;
|
|
111
|
+
const agentFile = path.join(agentsRoot, entry.name, 'config.json');
|
|
112
|
+
if (!fs.existsSync(agentFile)) continue;
|
|
113
|
+
summary.agentsScanned += 1;
|
|
114
|
+
const { value: input, text: agentText } = readJson(agentFile);
|
|
115
|
+
if (!isRecord(input)) throw new Error(`${agentFile}: Agent config must be an object`);
|
|
116
|
+
if (typeof input.$schema_version === 'number' && input.$schema_version > 5) {
|
|
117
|
+
throw new Error(`${agentFile}: unsupported future Agent schema version ${input.$schema_version}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rolesDir = path.join(agentsRoot, entry.name, 'roles');
|
|
121
|
+
const registryFile = path.join(rolesDir, 'index.json');
|
|
122
|
+
if (fs.existsSync(registryFile) && input.roles !== undefined) {
|
|
123
|
+
throw new Error(`${agentFile}: both config.roles and roles/index.json exist; resolve the ambiguous role source before migration`);
|
|
124
|
+
}
|
|
125
|
+
if (input.$schema_version === 5 && input.roles === undefined && fs.existsSync(registryFile)) {
|
|
126
|
+
const { value: registry } = readJson(registryFile);
|
|
127
|
+
validateOrThrow(validators.registry, registry, registryFile);
|
|
128
|
+
for (const roleId of registry.roles) {
|
|
129
|
+
const roleFile = path.join(rolesDir, `${roleId}.json`);
|
|
130
|
+
if (!fs.existsSync(roleFile)) continue;
|
|
131
|
+
const { value: roleValue, text: roleText } = readJson(roleFile);
|
|
132
|
+
const normalizedRole = normalizeRoleEffortCompat(roleValue);
|
|
133
|
+
validateRoleOrThrow(validators.role, normalizedRole, roleFile);
|
|
134
|
+
if (appendChange(changes, roleFile, 'role-config', roleText, normalizedRole)) {
|
|
135
|
+
summary.rolesWritten += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const policy = isRecord(input.roles) ? input.roles : {};
|
|
142
|
+
const definitions = isRecord(policy.definitions) ? policy.definitions : {};
|
|
143
|
+
for (const roleId of Object.keys(definitions)) {
|
|
144
|
+
if (!/^[a-z0-9_-]+$/.test(roleId) || MANAGEMENT_ROLES.has(roleId)) {
|
|
145
|
+
throw new Error(`${agentFile}: invalid role id ${JSON.stringify(roleId)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const roleIds = [...new Set([...BUILTIN_ROLES, ...Object.keys(definitions)])];
|
|
149
|
+
const defaultRoles = {
|
|
150
|
+
private: policy.defaultRoles?.private ?? null,
|
|
151
|
+
group: policy.defaultRoles?.group ?? null,
|
|
152
|
+
};
|
|
153
|
+
for (const scene of ['private', 'group']) {
|
|
154
|
+
const selected = defaultRoles[scene];
|
|
155
|
+
if (selected !== null && (MANAGEMENT_ROLES.has(selected) || !roleIds.includes(selected))) {
|
|
156
|
+
throw new Error(`${agentFile}: roles.defaultRoles.${scene} references an invalid role`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const registry = { $schema_version: 1, roles: roleIds, defaultRoles };
|
|
160
|
+
validateOrThrow(validators.registry, registry, registryFile);
|
|
161
|
+
|
|
162
|
+
const relationsDir = path.join(agentsRoot, entry.name, 'relations');
|
|
163
|
+
if (fs.existsSync(relationsDir)) {
|
|
164
|
+
const visit = dir => {
|
|
165
|
+
for (const relationEntry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
166
|
+
const full = path.join(dir, relationEntry.name);
|
|
167
|
+
if (relationEntry.isDirectory()) visit(full);
|
|
168
|
+
else if (relationEntry.isFile() && relationEntry.name === 'config.json') {
|
|
169
|
+
const relation = readJson(full).value;
|
|
170
|
+
if (relation.role !== undefined && relation.role !== null
|
|
171
|
+
&& (MANAGEMENT_ROLES.has(relation.role) || !roleIds.includes(relation.role))) {
|
|
172
|
+
throw new Error(`${full}: role references an invalid or unregistered role`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
visit(relationsDir);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const existingRegistry = fs.existsSync(registryFile) ? fs.readFileSync(registryFile, 'utf8') : null;
|
|
181
|
+
appendChange(changes, registryFile, 'role-registry', existingRegistry, registry);
|
|
182
|
+
for (const [roleId, definition] of Object.entries(definitions)) {
|
|
183
|
+
const roleFile = path.join(rolesDir, `${roleId}.json`);
|
|
184
|
+
const roleValue = normalizeRoleEffortCompat({ $schema_version: 1, ...definition });
|
|
185
|
+
validateRoleOrThrow(validators.role, roleValue, roleFile);
|
|
186
|
+
if (appendChange(changes, roleFile, 'role-config', fs.existsSync(roleFile) ? fs.readFileSync(roleFile, 'utf8') : null, roleValue)) {
|
|
187
|
+
summary.rolesWritten += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const output = { ...input, $schema_version: 5 };
|
|
192
|
+
delete output.roles;
|
|
193
|
+
validateOrThrow(validators.agent, output, agentFile);
|
|
194
|
+
appendChange(changes, agentFile, 'agent', agentText, output);
|
|
195
|
+
summary.agentsMigrated += 1;
|
|
196
|
+
}
|
|
197
|
+
return { home, changes, summary };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
200
|
function assertDaemonStopped(home) {
|
|
201
201
|
const candidates = [];
|
|
202
202
|
const pidFile = path.join(home, 'daemon.pid');
|
|
@@ -209,169 +209,169 @@ function assertDaemonStopped(home) {
|
|
|
209
209
|
} catch (error) {
|
|
210
210
|
if (error?.code !== 'ENOENT') throw error;
|
|
211
211
|
}
|
|
212
|
-
const instanceDir = path.join(home, 'data', 'instance');
|
|
213
|
-
if (fs.existsSync(instanceDir)) {
|
|
214
|
-
for (const name of fs.readdirSync(instanceDir)) {
|
|
215
|
-
if (!/^main-\d+\.json$/.test(name)) continue;
|
|
216
|
-
const file = path.join(instanceDir, name);
|
|
217
|
-
try { candidates.push({ pid: Number(JSON.parse(fs.readFileSync(file, 'utf8'))?.pid), source: file }); } catch {}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
for (const { pid, source } of candidates) {
|
|
221
|
-
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
222
|
-
try { process.kill(pid, 0); } catch (error) {
|
|
223
|
-
if (error?.code === 'ESRCH') continue;
|
|
224
|
-
if (error?.code !== 'EPERM') throw error;
|
|
225
|
-
}
|
|
226
|
-
throw new Error(`EvolCore daemon appears to be running (pid=${pid}, record=${source}); stop it before migration`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function acquireMigrationLock(home) {
|
|
231
|
-
const lockDir = path.join(home, 'data', 'role-v5-migration.lock');
|
|
232
|
-
const ownerFile = path.join(lockDir, 'owner.json');
|
|
233
|
-
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
234
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
235
|
-
try {
|
|
236
|
-
fs.mkdirSync(lockDir);
|
|
237
|
-
fs.writeFileSync(ownerFile, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`);
|
|
238
|
-
return () => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
239
|
-
} catch (error) {
|
|
240
|
-
if (error?.code !== 'EEXIST') throw error;
|
|
241
|
-
let ownerPid;
|
|
242
|
-
try { ownerPid = Number(JSON.parse(fs.readFileSync(ownerFile, 'utf8'))?.pid); }
|
|
243
|
-
catch { throw new Error(`Role config migration lock is unreadable: ${lockDir}`); }
|
|
244
|
-
if (Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
245
|
-
try {
|
|
246
|
-
process.kill(ownerPid, 0);
|
|
247
|
-
throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
|
|
248
|
-
} catch (ownerError) {
|
|
249
|
-
if (ownerError?.code === 'EPERM') throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
|
|
250
|
-
if (ownerError?.code !== 'ESRCH') throw ownerError;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
throw new Error(`Unable to acquire role config migration lock: ${lockDir}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function atomicWrite(file, content) {
|
|
260
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
261
|
-
const mode = fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600;
|
|
262
|
-
const temp = `${file}.role-v5-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
|
|
263
|
-
fs.writeFileSync(temp, content, { mode });
|
|
264
|
-
fs.renameSync(temp, file);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function migrationJournalPath(home) {
|
|
268
|
-
return path.join(home, 'data', 'role-v5-migration.json');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function writeMigrationJournal(plan, state) {
|
|
272
|
-
atomicWrite(migrationJournalPath(plan.home), `${JSON.stringify({
|
|
273
|
-
schemaVersion: 1,
|
|
274
|
-
state,
|
|
275
|
-
home: plan.home,
|
|
276
|
-
changes: plan.changes,
|
|
277
|
-
}, null, 2)}\n`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function removeMigrationJournal(home) {
|
|
281
|
-
fs.rmSync(migrationJournalPath(home), { force: true });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function recoverMigrationJournal(home) {
|
|
285
|
-
const file = migrationJournalPath(home);
|
|
286
|
-
if (!fs.existsSync(file)) return;
|
|
287
|
-
let journal;
|
|
288
|
-
try { journal = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
289
|
-
catch { throw new Error(`Role config v5 migration journal is unreadable: ${file}`); }
|
|
290
|
-
if (journal?.schemaVersion !== 1
|
|
291
|
-
|| !['prepared', 'committed'].includes(journal.state)
|
|
292
|
-
|| path.resolve(journal.home) !== home
|
|
293
|
-
|| !Array.isArray(journal.changes)) {
|
|
294
|
-
throw new Error(`Role config v5 migration journal is invalid: ${file}`);
|
|
295
|
-
}
|
|
296
|
-
const rootPrefix = home + path.sep;
|
|
297
|
-
for (const item of journal.changes) {
|
|
298
|
-
const target = path.resolve(item?.file || '');
|
|
299
|
-
if (!target.startsWith(rootPrefix)
|
|
300
|
-
|| (item.before !== null && typeof item.before !== 'string')
|
|
301
|
-
|| typeof item.after !== 'string') {
|
|
302
|
-
throw new Error(`Role config v5 migration journal contains an unsafe change: ${file}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (journal.state === 'prepared') {
|
|
306
|
-
for (const item of [...journal.changes].reverse()) {
|
|
307
|
-
if (item.before === null) fs.rmSync(item.file, { force: true });
|
|
308
|
-
else atomicWrite(item.file, item.before);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
removeMigrationJournal(home);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function applyMigration(plan) {
|
|
315
|
-
if (!plan.changes.length) return null;
|
|
316
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
317
|
-
const backup = path.join(plan.home, 'backups', 'role-v5-migration', stamp);
|
|
318
|
-
fs.mkdirSync(backup, { recursive: true });
|
|
319
|
-
for (const item of plan.changes) {
|
|
320
|
-
if (item.before === null) continue;
|
|
321
|
-
const dest = path.join(backup, 'files', path.relative(plan.home, item.file));
|
|
322
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
323
|
-
fs.writeFileSync(dest, item.before);
|
|
324
|
-
}
|
|
325
|
-
fs.writeFileSync(path.join(backup, 'manifest.json'), `${JSON.stringify({
|
|
326
|
-
schemaVersion: 1,
|
|
327
|
-
createdAt: new Date().toISOString(),
|
|
328
|
-
summary: plan.summary,
|
|
329
|
-
files: plan.changes.map(item => ({ path: path.relative(plan.home, item.file), kind: item.kind })),
|
|
330
|
-
}, null, 2)}\n`);
|
|
331
|
-
writeMigrationJournal(plan, 'prepared');
|
|
332
|
-
try {
|
|
333
|
-
for (const item of plan.changes) {
|
|
334
|
-
atomicWrite(item.file, item.after);
|
|
335
|
-
}
|
|
336
|
-
writeMigrationJournal(plan, 'committed');
|
|
337
|
-
removeMigrationJournal(plan.home);
|
|
338
|
-
} catch (error) {
|
|
339
|
-
recoverMigrationJournal(plan.home);
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
return backup;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function runMigration(options = {}) {
|
|
346
|
-
const home = resolveHome(options.home);
|
|
347
|
-
const releaseLock = acquireMigrationLock(home);
|
|
348
|
-
try {
|
|
349
|
-
assertDaemonStopped(home);
|
|
350
|
-
recoverMigrationJournal(home);
|
|
351
|
-
const plan = planMigration(home);
|
|
352
|
-
const backup = options.apply ? applyMigration(plan) : null;
|
|
353
|
-
return { ok: true, mode: options.apply ? 'applied' : 'dry-run', home, changedFiles: plan.changes.length, backup, summary: plan.summary };
|
|
354
|
-
} finally {
|
|
355
|
-
releaseLock();
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function main() {
|
|
360
|
-
try {
|
|
361
|
-
const options = parseArgs(process.argv.slice(2));
|
|
362
|
-
if (options.help) {
|
|
363
|
-
console.log('Usage: node kits/migrations/migrate-role-config-v5.mjs [--apply] [--home <path>] [--json]');
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const result = runMigration(options);
|
|
367
|
-
if (options.json) console.log(JSON.stringify(result));
|
|
368
|
-
else console.log(`Role config v5 migration (${result.mode}): ${result.changedFiles} file(s)`);
|
|
369
|
-
} catch (error) {
|
|
370
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
371
|
-
if (process.argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: message }));
|
|
372
|
-
else console.error(`Migration failed: ${message}`);
|
|
373
|
-
process.exitCode = 1;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
212
|
+
const instanceDir = path.join(home, 'data', 'instance');
|
|
213
|
+
if (fs.existsSync(instanceDir)) {
|
|
214
|
+
for (const name of fs.readdirSync(instanceDir)) {
|
|
215
|
+
if (!/^main-\d+\.json$/.test(name)) continue;
|
|
216
|
+
const file = path.join(instanceDir, name);
|
|
217
|
+
try { candidates.push({ pid: Number(JSON.parse(fs.readFileSync(file, 'utf8'))?.pid), source: file }); } catch {}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const { pid, source } of candidates) {
|
|
221
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
222
|
+
try { process.kill(pid, 0); } catch (error) {
|
|
223
|
+
if (error?.code === 'ESRCH') continue;
|
|
224
|
+
if (error?.code !== 'EPERM') throw error;
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`EvolCore daemon appears to be running (pid=${pid}, record=${source}); stop it before migration`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function acquireMigrationLock(home) {
|
|
231
|
+
const lockDir = path.join(home, 'data', 'role-v5-migration.lock');
|
|
232
|
+
const ownerFile = path.join(lockDir, 'owner.json');
|
|
233
|
+
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
234
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
235
|
+
try {
|
|
236
|
+
fs.mkdirSync(lockDir);
|
|
237
|
+
fs.writeFileSync(ownerFile, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`);
|
|
238
|
+
return () => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
241
|
+
let ownerPid;
|
|
242
|
+
try { ownerPid = Number(JSON.parse(fs.readFileSync(ownerFile, 'utf8'))?.pid); }
|
|
243
|
+
catch { throw new Error(`Role config migration lock is unreadable: ${lockDir}`); }
|
|
244
|
+
if (Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
245
|
+
try {
|
|
246
|
+
process.kill(ownerPid, 0);
|
|
247
|
+
throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
|
|
248
|
+
} catch (ownerError) {
|
|
249
|
+
if (ownerError?.code === 'EPERM') throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
|
|
250
|
+
if (ownerError?.code !== 'ESRCH') throw ownerError;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`Unable to acquire role config migration lock: ${lockDir}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function atomicWrite(file, content) {
|
|
260
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
261
|
+
const mode = fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600;
|
|
262
|
+
const temp = `${file}.role-v5-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
|
|
263
|
+
fs.writeFileSync(temp, content, { mode });
|
|
264
|
+
fs.renameSync(temp, file);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function migrationJournalPath(home) {
|
|
268
|
+
return path.join(home, 'data', 'role-v5-migration.json');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function writeMigrationJournal(plan, state) {
|
|
272
|
+
atomicWrite(migrationJournalPath(plan.home), `${JSON.stringify({
|
|
273
|
+
schemaVersion: 1,
|
|
274
|
+
state,
|
|
275
|
+
home: plan.home,
|
|
276
|
+
changes: plan.changes,
|
|
277
|
+
}, null, 2)}\n`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function removeMigrationJournal(home) {
|
|
281
|
+
fs.rmSync(migrationJournalPath(home), { force: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function recoverMigrationJournal(home) {
|
|
285
|
+
const file = migrationJournalPath(home);
|
|
286
|
+
if (!fs.existsSync(file)) return;
|
|
287
|
+
let journal;
|
|
288
|
+
try { journal = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
289
|
+
catch { throw new Error(`Role config v5 migration journal is unreadable: ${file}`); }
|
|
290
|
+
if (journal?.schemaVersion !== 1
|
|
291
|
+
|| !['prepared', 'committed'].includes(journal.state)
|
|
292
|
+
|| path.resolve(journal.home) !== home
|
|
293
|
+
|| !Array.isArray(journal.changes)) {
|
|
294
|
+
throw new Error(`Role config v5 migration journal is invalid: ${file}`);
|
|
295
|
+
}
|
|
296
|
+
const rootPrefix = home + path.sep;
|
|
297
|
+
for (const item of journal.changes) {
|
|
298
|
+
const target = path.resolve(item?.file || '');
|
|
299
|
+
if (!target.startsWith(rootPrefix)
|
|
300
|
+
|| (item.before !== null && typeof item.before !== 'string')
|
|
301
|
+
|| typeof item.after !== 'string') {
|
|
302
|
+
throw new Error(`Role config v5 migration journal contains an unsafe change: ${file}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (journal.state === 'prepared') {
|
|
306
|
+
for (const item of [...journal.changes].reverse()) {
|
|
307
|
+
if (item.before === null) fs.rmSync(item.file, { force: true });
|
|
308
|
+
else atomicWrite(item.file, item.before);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
removeMigrationJournal(home);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function applyMigration(plan) {
|
|
315
|
+
if (!plan.changes.length) return null;
|
|
316
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
317
|
+
const backup = path.join(plan.home, 'backups', 'role-v5-migration', stamp);
|
|
318
|
+
fs.mkdirSync(backup, { recursive: true });
|
|
319
|
+
for (const item of plan.changes) {
|
|
320
|
+
if (item.before === null) continue;
|
|
321
|
+
const dest = path.join(backup, 'files', path.relative(plan.home, item.file));
|
|
322
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
323
|
+
fs.writeFileSync(dest, item.before);
|
|
324
|
+
}
|
|
325
|
+
fs.writeFileSync(path.join(backup, 'manifest.json'), `${JSON.stringify({
|
|
326
|
+
schemaVersion: 1,
|
|
327
|
+
createdAt: new Date().toISOString(),
|
|
328
|
+
summary: plan.summary,
|
|
329
|
+
files: plan.changes.map(item => ({ path: path.relative(plan.home, item.file), kind: item.kind })),
|
|
330
|
+
}, null, 2)}\n`);
|
|
331
|
+
writeMigrationJournal(plan, 'prepared');
|
|
332
|
+
try {
|
|
333
|
+
for (const item of plan.changes) {
|
|
334
|
+
atomicWrite(item.file, item.after);
|
|
335
|
+
}
|
|
336
|
+
writeMigrationJournal(plan, 'committed');
|
|
337
|
+
removeMigrationJournal(plan.home);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
recoverMigrationJournal(plan.home);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
return backup;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function runMigration(options = {}) {
|
|
346
|
+
const home = resolveHome(options.home);
|
|
347
|
+
const releaseLock = acquireMigrationLock(home);
|
|
348
|
+
try {
|
|
349
|
+
assertDaemonStopped(home);
|
|
350
|
+
recoverMigrationJournal(home);
|
|
351
|
+
const plan = planMigration(home);
|
|
352
|
+
const backup = options.apply ? applyMigration(plan) : null;
|
|
353
|
+
return { ok: true, mode: options.apply ? 'applied' : 'dry-run', home, changedFiles: plan.changes.length, backup, summary: plan.summary };
|
|
354
|
+
} finally {
|
|
355
|
+
releaseLock();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function main() {
|
|
360
|
+
try {
|
|
361
|
+
const options = parseArgs(process.argv.slice(2));
|
|
362
|
+
if (options.help) {
|
|
363
|
+
console.log('Usage: node kits/migrations/migrate-role-config-v5.mjs [--apply] [--home <path>] [--json]');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const result = runMigration(options);
|
|
367
|
+
if (options.json) console.log(JSON.stringify(result));
|
|
368
|
+
else console.log(`Role config v5 migration (${result.mode}): ${result.changedFiles} file(s)`);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
371
|
+
if (process.argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: message }));
|
|
372
|
+
else console.error(`Migration failed: ${message}`);
|
|
373
|
+
process.exitCode = 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|