openyida 2026.10.13 → 2026.10.14
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 +76 -23
- package/bin/yida.js +32 -2
- package/lib/agent/PROTOCOL.md +201 -0
- package/lib/agent/ask-human-help.js +19 -0
- package/lib/agent/ask-human.js +222 -0
- package/lib/agent/cmd.js +405 -0
- package/lib/agent/command-receipt.js +84 -0
- package/lib/agent/connection-store.js +148 -0
- package/lib/agent/diagnostic-log.js +133 -0
- package/lib/agent/frozen-bundle.js +298 -0
- package/lib/agent/managed-command-map.js +47 -0
- package/lib/agent/managed-context.js +40 -0
- package/lib/agent/managed-run.js +84 -0
- package/lib/agent/mutation-receipt.js +82 -0
- package/lib/agent/private-directory.js +63 -0
- package/lib/agent/provider-preflight.js +257 -0
- package/lib/agent/providers.js +4 -0
- package/lib/agent/runtime-catalog.json +42 -0
- package/lib/agent/runtime-manifest.js +96 -0
- package/lib/agent/runtime-package.js +155 -0
- package/lib/agent/stdio.js +321 -0
- package/lib/app/check-prd-completeness.js +15 -4
- package/lib/app/create-form/batch.js +30 -9
- package/lib/app/create-form.js +7 -2
- package/lib/app/custom-theme.js +13 -3
- package/lib/app/get-schema.js +6 -1
- package/lib/app/update-app.js +13 -1
- package/lib/app/update-form-config.js +71 -80
- package/lib/asset/asset-plan.js +6 -4
- package/lib/auth/profile-lock.js +89 -0
- package/lib/auth/token-auth.js +57 -6
- package/lib/auth/token-store.js +26 -2
- package/lib/core/command-manifest.js +202 -118
- package/lib/core/locales/en.js +48 -4
- package/lib/core/locales/zh.js +48 -4
- package/lib/core/managed-policy.js +88 -0
- package/lib/core/query-data.js +22 -3
- package/lib/core/sample.js +1 -0
- package/lib/core/update.js +3 -1
- package/lib/core/utils.js +34 -1
- package/lib/design-plan/materialize.js +4 -21
- package/lib/design-plan/render-html.js +299 -0
- package/lib/page-config/save-share-config.js +16 -5
- package/lib/samples/yida-data-management/empty-query.js +69 -0
- package/package.json +10 -1
- package/scripts/postinstall.js +2 -2
- package/yida-skills/SKILL.md +32 -2
- package/yida-skills/references/local-agent-diagnostics.md +154 -0
- package/yida-skills/references/local-agent-preflight.md +56 -0
- package/yida-skills/references/query-condition-guide.md +223 -195
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +11 -3
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +8 -5
- package/yida-skills/skills/yida-data-management/SKILL.md +64 -19
- package/yida-skills/skills/yida-data-management/references/data-format-guide.md +61 -25
- package/yida-skills/skills/yida-data-source-connectors/SKILL.md +6 -0
- package/yida-skills/skills/yida-design/SKILL.md +2 -0
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +2 -0
- package/yida-skills/skills/yida-design/templates/design-themes/README.md +1 -1
- package/yida-skills/skills/yida-document-markdown/SKILL.md +6 -0
- package/yida-skills/skills/yida-image-assets/SKILL.md +1 -1
- package/yida-skills/skills/yida-tingji/SKILL.md +4 -0
- package/yida-skills/skills-index.json +52 -7
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { managedError } = require('./managed-context');
|
|
7
|
+
const { readManagedContext } = require('./managed-context');
|
|
8
|
+
const { beginManagedMutationCapture, classifyManagedMutation } = require('./mutation-receipt');
|
|
9
|
+
|
|
10
|
+
const MAX_RECEIPT_BYTES = 16 * 1024;
|
|
11
|
+
|
|
12
|
+
function retryReceiptFileOperation(operation) {
|
|
13
|
+
const delays = [20, 40, 80, 160, 320];
|
|
14
|
+
for (let attempt = 0; ; attempt++) {
|
|
15
|
+
try {return operation();} catch (error) {
|
|
16
|
+
if (process.platform !== 'win32' || attempt === delays.length ||
|
|
17
|
+
!['EBUSY', 'EPERM', 'EACCES'].includes(error.code)) {throw error;}
|
|
18
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeReceipt(directory, receipt) {
|
|
24
|
+
const bytes = JSON.stringify(receipt);
|
|
25
|
+
if (Buffer.byteLength(bytes) > MAX_RECEIPT_BYTES) {throw managedError('MANAGED_RECEIPT_INVALID', 'managed_receipt_invalid');}
|
|
26
|
+
const temporary = path.join(directory, `.pending-${crypto.randomUUID()}`);
|
|
27
|
+
let fd;
|
|
28
|
+
let failure;
|
|
29
|
+
try {
|
|
30
|
+
fd = fs.openSync(temporary, 'wx', 0o600);
|
|
31
|
+
fs.writeFileSync(fd, bytes);
|
|
32
|
+
fs.fsyncSync(fd);
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
fd = undefined;
|
|
35
|
+
retryReceiptFileOperation(() => fs.renameSync(temporary, path.join(directory, `${receipt.id}.json`)));
|
|
36
|
+
if (process.platform !== 'win32') {
|
|
37
|
+
const parent = fs.openSync(directory, 'r');
|
|
38
|
+
try {fs.fsyncSync(parent);} finally {fs.closeSync(parent);}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
failure = error;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
if (fd !== undefined) {fs.closeSync(fd);}
|
|
45
|
+
if (fs.existsSync(temporary)) {retryReceiptFileOperation(() => fs.unlinkSync(temporary));}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Preserve the publish error if cleanup also encounters a Windows lock.
|
|
48
|
+
if (!failure) {failure = error;}
|
|
49
|
+
}
|
|
50
|
+
if (failure) {throw failure;}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// One bounded receipt only for an explicit successful Yida write. No argv,
|
|
54
|
+
// stdout, source, credentials, or crash-time intent record is persisted.
|
|
55
|
+
function beginCommandReceipt({ command, args = [], env = process.env, consoleObject = console } = {}) {
|
|
56
|
+
if (env.OPENYIDA_MANAGED_RUN !== '1') {return { finish() {} };}
|
|
57
|
+
// Only an allowlisted Yida write gets a durable receipt. Reads, help and
|
|
58
|
+
// ordinary CLI bookkeeping must not create files in the attempt directory.
|
|
59
|
+
if (!classifyManagedMutation(command, args)) {return { finish() {} };}
|
|
60
|
+
const context = readManagedContext(env);
|
|
61
|
+
const directory = env.OPENYIDA_AGENT_RECEIPTS_DIR;
|
|
62
|
+
if (!directory || !path.isAbsolute(directory) || fs.realpathSync(directory) !== path.resolve(directory)) {
|
|
63
|
+
throw managedError('MANAGED_RECEIPT_INVALID', 'managed_receipt_invalid');
|
|
64
|
+
}
|
|
65
|
+
const info = fs.lstatSync(directory);
|
|
66
|
+
if (!info.isDirectory() || info.isSymbolicLink() || process.platform !== 'win32' && (info.mode & 0o077)) {
|
|
67
|
+
throw managedError('MANAGED_RECEIPT_INVALID', 'managed_receipt_invalid');
|
|
68
|
+
}
|
|
69
|
+
if (fs.readdirSync(directory).length >= 4000) {throw managedError('MANAGED_RECEIPT_INVALID', 'managed_receipt_invalid');}
|
|
70
|
+
const receipt = { schemaVersion: 1, id: crypto.randomUUID(), runId: context.runId,
|
|
71
|
+
attemptId: context.attemptId };
|
|
72
|
+
const capture = beginManagedMutationCapture({ command, args, env, consoleObject, eventId: receipt.id });
|
|
73
|
+
let finished = false;
|
|
74
|
+
return {
|
|
75
|
+
finish(exitCode = 0) {
|
|
76
|
+
if (finished) {return;}
|
|
77
|
+
finished = true;
|
|
78
|
+
const mutation = exitCode === 0 ? capture.commit() : (capture.discard(), null);
|
|
79
|
+
if (mutation) {writeReceipt(directory, { ...receipt, mutation });}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { beginCommandReceipt, writeReceipt, MAX_RECEIPT_BYTES };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { CliError } = require('../core/cli-error');
|
|
7
|
+
const { t } = require('../core/i18n');
|
|
8
|
+
const { mkdirPrivate } = require('./private-directory');
|
|
9
|
+
|
|
10
|
+
const ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
11
|
+
|
|
12
|
+
function failure(code = 'AGENT_CONNECTION_STATE_INVALID') {
|
|
13
|
+
return new CliError(t('agent.identity_invalid'), { code });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function privateDirectory(directory) {
|
|
17
|
+
mkdirPrivate(directory);
|
|
18
|
+
const stat = fs.lstatSync(directory);
|
|
19
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {throw failure();}
|
|
20
|
+
if (process.platform !== 'win32') {
|
|
21
|
+
fs.chmodSync(directory, 0o700);
|
|
22
|
+
if ((fs.statSync(directory).mode & 0o077) !== 0) {throw failure();}
|
|
23
|
+
}
|
|
24
|
+
return directory;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson(file, allowedKeys) {
|
|
28
|
+
const stat = fs.lstatSync(file);
|
|
29
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 32 * 1024) {throw failure();}
|
|
30
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {throw failure();}
|
|
31
|
+
let value;
|
|
32
|
+
try {value = JSON.parse(fs.readFileSync(file, 'utf8'));} catch {throw failure();}
|
|
33
|
+
if (!value || Array.isArray(value) || typeof value !== 'object' || Object.keys(value).some(key => !allowedKeys.includes(key))) {throw failure();}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function atomicJson(file, value) {
|
|
38
|
+
const directory = privateDirectory(path.dirname(file));
|
|
39
|
+
const temporary = path.join(directory, `.pending-${process.pid}-${crypto.randomBytes(12).toString('hex')}`);
|
|
40
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
|
|
41
|
+
fs.renameSync(temporary, file);
|
|
42
|
+
if (process.platform !== 'win32') {fs.chmodSync(file, 0o600);}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function storedEndpoint(value) {
|
|
46
|
+
let endpoint;
|
|
47
|
+
try {endpoint = new URL(value);} catch {throw failure();}
|
|
48
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(endpoint.hostname);
|
|
49
|
+
if ((endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && loopback)) ||
|
|
50
|
+
endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {throw failure();}
|
|
51
|
+
return endpoint.toString().replace(/\/+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function installation(root) {
|
|
55
|
+
privateDirectory(root);
|
|
56
|
+
const file = path.join(root, 'installation.json');
|
|
57
|
+
if (fs.existsSync(file)) {
|
|
58
|
+
const value = readJson(file, ['schemaVersion', 'installationId']);
|
|
59
|
+
if (value.schemaVersion !== 1 || !ID.test(value.installationId || '')) {throw failure();}
|
|
60
|
+
return value.installationId;
|
|
61
|
+
}
|
|
62
|
+
const installationId = crypto.randomUUID();
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(file, `${JSON.stringify({ schemaVersion: 1, installationId })}\n`, { mode: 0o600, flag: 'wx' });
|
|
65
|
+
return installationId;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error && error.code === 'EEXIST') {return installation(root);}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function enrollmentSlot(token) {
|
|
73
|
+
const enrollmentId = typeof token === 'string' ? token.split('.', 1)[0] : '';
|
|
74
|
+
return ID.test(enrollmentId) ? enrollmentId : crypto.randomUUID();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function prepareConnection(root, enrollmentToken) {
|
|
78
|
+
const installationId = installation(root);
|
|
79
|
+
const connections = privateDirectory(path.join(root, 'connections'));
|
|
80
|
+
const stateDir = privateDirectory(path.join(connections, enrollmentSlot(enrollmentToken)));
|
|
81
|
+
const publicState = path.join(stateDir, 'device.json');
|
|
82
|
+
if (fs.existsSync(publicState)) {
|
|
83
|
+
const existing = readJson(publicState, [
|
|
84
|
+
'installationId', 'endpoint', 'endpointId', 'deviceId', 'devicePublicKey',
|
|
85
|
+
'expiresAt', 'refreshExpiresAt', 'credentialVersion', 'refreshActionId',
|
|
86
|
+
]);
|
|
87
|
+
if (existing.installationId !== installationId) {throw failure();}
|
|
88
|
+
} else {
|
|
89
|
+
atomicJson(publicState, { installationId });
|
|
90
|
+
}
|
|
91
|
+
return { installationId, stateDir };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function listConnections(root) {
|
|
95
|
+
if (!fs.existsSync(root)) {return [];}
|
|
96
|
+
const rootStat = fs.lstatSync(root);
|
|
97
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || (process.platform !== 'win32' && (rootStat.mode & 0o077) !== 0)) {throw failure();}
|
|
98
|
+
const installationFile = path.join(root, 'installation.json');
|
|
99
|
+
const connections = path.join(root, 'connections');
|
|
100
|
+
if (!fs.existsSync(installationFile) || !fs.existsSync(connections)) {return [];}
|
|
101
|
+
const installationState = readJson(installationFile, ['schemaVersion', 'installationId']);
|
|
102
|
+
if (installationState.schemaVersion !== 1 || !ID.test(installationState.installationId || '')) {throw failure();}
|
|
103
|
+
const installId = installationState.installationId;
|
|
104
|
+
const connectionsStat = fs.lstatSync(connections);
|
|
105
|
+
if (!connectionsStat.isDirectory() || connectionsStat.isSymbolicLink() || (process.platform !== 'win32' && (connectionsStat.mode & 0o077) !== 0)) {throw failure();}
|
|
106
|
+
const byConnection = new Map();
|
|
107
|
+
for (const name of fs.readdirSync(connections).sort()) {
|
|
108
|
+
if (!ID.test(name)) {continue;}
|
|
109
|
+
const stateDir = path.join(connections, name);
|
|
110
|
+
let stat;
|
|
111
|
+
try {stat = fs.lstatSync(stateDir);} catch {continue;}
|
|
112
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {throw failure();}
|
|
113
|
+
const file = path.join(stateDir, 'device.json');
|
|
114
|
+
if (!fs.existsSync(file)) {continue;}
|
|
115
|
+
const value = readJson(file, [
|
|
116
|
+
'installationId', 'endpoint', 'endpointId', 'deviceId', 'devicePublicKey',
|
|
117
|
+
'expiresAt', 'refreshExpiresAt', 'credentialVersion', 'refreshActionId',
|
|
118
|
+
]);
|
|
119
|
+
if (value.installationId !== installId) {throw failure();}
|
|
120
|
+
if (!value.deviceId) {continue;}
|
|
121
|
+
if (!ID.test(value.deviceId) || !ID.test(value.endpointId || '')) {throw failure();}
|
|
122
|
+
const candidate = {
|
|
123
|
+
installationId: installId,
|
|
124
|
+
stateDir,
|
|
125
|
+
endpoint: storedEndpoint(value.endpoint),
|
|
126
|
+
endpointId: value.endpointId,
|
|
127
|
+
connectionId: value.deviceId,
|
|
128
|
+
credentialVersion: Number.isSafeInteger(value.credentialVersion) ? value.credentialVersion : 0,
|
|
129
|
+
modifiedAt: fs.statSync(file).mtimeMs,
|
|
130
|
+
};
|
|
131
|
+
const previous = byConnection.get(candidate.connectionId);
|
|
132
|
+
if (!previous || candidate.credentialVersion > previous.credentialVersion ||
|
|
133
|
+
(candidate.credentialVersion === previous.credentialVersion && candidate.modifiedAt > previous.modifiedAt)) {
|
|
134
|
+
byConnection.set(candidate.connectionId, candidate);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...byConnection.values()]
|
|
138
|
+
.sort((a, b) => a.connectionId.localeCompare(b.connectionId))
|
|
139
|
+
.map(connection => ({
|
|
140
|
+
installationId: connection.installationId,
|
|
141
|
+
stateDir: connection.stateDir,
|
|
142
|
+
endpoint: connection.endpoint,
|
|
143
|
+
endpointId: connection.endpointId,
|
|
144
|
+
connectionId: connection.connectionId,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { installation, prepareConnection, listConnections };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { redactString } = require('../core/redact');
|
|
7
|
+
const { mkdirPrivate } = require('./private-directory');
|
|
8
|
+
|
|
9
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
const MAX_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
11
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const MAX_FILES = 256;
|
|
13
|
+
const FIELDS = new Set([
|
|
14
|
+
'command', 'component', 'errorCode', 'statusCode', 'closeCode', 'endpointId',
|
|
15
|
+
'provider', 'runtimeId', 'sessionId', 'runId', 'attemptId', 'phase', 'status',
|
|
16
|
+
'runtimeVersion', 'providerVersion',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function isProductLog(name) {
|
|
20
|
+
return /^openyida-local-agent-[a-z0-9_-]+-\d{8}T\d{6}Z-\d+(?:\.\d+)?\.jsonl$/.test(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanup(dir, current) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const files = [];
|
|
26
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
if (!entry.isFile() || !isProductLog(entry.name)) {continue;}
|
|
28
|
+
const target = path.join(dir, entry.name);
|
|
29
|
+
const stat = fs.statSync(target);
|
|
30
|
+
if (now - stat.mtimeMs > MAX_AGE_MS) {
|
|
31
|
+
try {fs.unlinkSync(target);} catch { /* best-effort diagnostic cleanup */ }
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
files.push({ target, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
35
|
+
}
|
|
36
|
+
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
37
|
+
let total = files.reduce((sum, file) => sum + file.size, 0);
|
|
38
|
+
let remaining = files.length;
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (remaining <= MAX_FILES && total <= MAX_TOTAL_BYTES) {break;}
|
|
41
|
+
if (file.target === current) {continue;}
|
|
42
|
+
try {fs.unlinkSync(file.target); total -= file.size; remaining--;} catch { /* retry next launch */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createDiagnosticLog(dir) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirPrivate(dir);
|
|
49
|
+
if (!fs.lstatSync(dir).isDirectory()) {return () => {};}
|
|
50
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
51
|
+
const file = path.join(dir, `openyida-local-agent-launcher-${stamp}-${process.pid}.jsonl`);
|
|
52
|
+
fs.closeSync(fs.openSync(file, 'a', 0o600));
|
|
53
|
+
cleanup(dir, file);
|
|
54
|
+
return (level, event, values = {}) => {
|
|
55
|
+
try {
|
|
56
|
+
if (!/^[a-z0-9_]{1,100}$/.test(event)) {return;}
|
|
57
|
+
const record = { schema: 'openyida.local_agent.diagnostic.v1', timestamp: new Date().toISOString(), level, event, os: process.platform, arch: process.arch };
|
|
58
|
+
for (const [key, value] of Object.entries(values)) {
|
|
59
|
+
if (!FIELDS.has(key)) {continue;}
|
|
60
|
+
if (typeof value === 'string') {record[key] = redactString(value.slice(0, 512));}
|
|
61
|
+
else if (typeof value === 'boolean' || typeof value === 'number') {record[key] = value;}
|
|
62
|
+
}
|
|
63
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
64
|
+
const currentSize = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
65
|
+
if (Buffer.byteLength(line) <= 8192 && currentSize + Buffer.byteLength(line) <= MAX_FILE_BYTES) {
|
|
66
|
+
fs.appendFileSync(file, line, { encoding: 'utf8', mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
} catch { /* diagnostics must never affect the product path */ }
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return () => {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function defaultDiagnosticDir(homedir = os.homedir()) {
|
|
76
|
+
return path.join(homedir, '.openyida', 'local-agent', 'logs');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Read only product diagnostic JSONL, not device state, journals or credentials.
|
|
80
|
+
// Failed enrollment has no completed Connection, so read the installation log
|
|
81
|
+
// directory rather than depending on listConnections finding a paired device.
|
|
82
|
+
function readRecentCredentialFailures(dir, now = Date.now()) {
|
|
83
|
+
const reasons = new Set(['protocol_invalid', 'device_invalid', 'access_format_invalid',
|
|
84
|
+
'refresh_format_invalid', 'version_invalid', 'access_expired', 'refresh_expired', 'rotation_binding_invalid']);
|
|
85
|
+
const results = [];
|
|
86
|
+
try {
|
|
87
|
+
const directory = fs.lstatSync(dir);
|
|
88
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {return results;}
|
|
89
|
+
const files = fs.readdirSync(dir, { withFileTypes: true })
|
|
90
|
+
.filter(entry => entry.isFile() && entry.name.startsWith('openyida-local-agent-runtime-') && isProductLog(entry.name))
|
|
91
|
+
.flatMap(entry => {
|
|
92
|
+
try {return [{ file: path.join(dir, entry.name), stat: fs.lstatSync(path.join(dir, entry.name)) }];}
|
|
93
|
+
catch {return [];}
|
|
94
|
+
})
|
|
95
|
+
.filter(item => item.stat.isFile() && !item.stat.isSymbolicLink() &&
|
|
96
|
+
item.stat.size <= MAX_FILE_BYTES && item.stat.mtimeMs >= now - 2 * 60 * 60 * 1000)
|
|
97
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs).slice(0, 8);
|
|
98
|
+
for (const { file, stat: snapshot } of files) {
|
|
99
|
+
let raw;
|
|
100
|
+
let fd;
|
|
101
|
+
try {
|
|
102
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
103
|
+
const stat = fs.fstatSync(fd);
|
|
104
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.ino !== snapshot.ino || stat.dev !== snapshot.dev) {continue;}
|
|
105
|
+
const data = Buffer.alloc(stat.size);
|
|
106
|
+
const bytes = fs.readSync(fd, data, 0, data.length, 0);
|
|
107
|
+
raw = data.subarray(0, bytes).toString('utf8');
|
|
108
|
+
} catch {continue;} finally {if (fd !== undefined) {fs.closeSync(fd);}}
|
|
109
|
+
for (const line of raw.split('\n')) {
|
|
110
|
+
if (!line || Buffer.byteLength(line) > 8192) {continue;}
|
|
111
|
+
let record;
|
|
112
|
+
try {record = JSON.parse(line);} catch {continue;}
|
|
113
|
+
if (!record || record.schema !== 'openyida.local_agent.diagnostic.v1' ||
|
|
114
|
+
!['runtime_failed', 'connection_error'].includes(record.event) || record.errorCode !== 'DEVICE_CREDENTIAL_RESPONSE_INVALID') {continue;}
|
|
115
|
+
const timestamp = Date.parse(record.timestamp);
|
|
116
|
+
if (!Number.isFinite(timestamp) || timestamp < now - 2 * 60 * 60 * 1000 || timestamp > now + 5 * 60 * 1000) {continue;}
|
|
117
|
+
const safe = { timestamp: new Date(timestamp).toISOString(), errorCode: 'DEVICE_CREDENTIAL_RESPONSE_INVALID', historical: true };
|
|
118
|
+
if (reasons.has(record.credentialReason)) {safe.credentialReason = record.credentialReason;}
|
|
119
|
+
if (['connect', 'run', 'reauthenticate'].includes(record.phase)) {safe.phase = record.phase;}
|
|
120
|
+
if (typeof record.endpointId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(record.endpointId)) {safe.endpointId = redactString(record.endpointId);}
|
|
121
|
+
for (const key of ['accessRemainingSeconds', 'refreshRemainingSeconds']) {
|
|
122
|
+
if (Number.isSafeInteger(record[key])) {safe[key] = record[key];}
|
|
123
|
+
}
|
|
124
|
+
results.push(safe);
|
|
125
|
+
results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
126
|
+
if (results.length > 5) {results.length = 5;}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch { /* Log diagnostics are optional, including while files rotate. */ }
|
|
130
|
+
return results.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 5);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { createDiagnosticLog, defaultDiagnosticDir, isProductLog, readRecentCredentialFailures };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { CliError } = require('../core/cli-error');
|
|
6
|
+
const { t } = require('../core/i18n');
|
|
7
|
+
const { digest, readRegularFile, platformPackageName } = require('./runtime-package');
|
|
8
|
+
const { parseStrictJson, PRODUCTION_RUNTIME_PLATFORMS } = require('./runtime-manifest');
|
|
9
|
+
|
|
10
|
+
const MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
11
|
+
const MAX_BUNDLE_BYTES = 1024 * 1024 * 1024;
|
|
12
|
+
const MAX_FILES = 30000;
|
|
13
|
+
const MAX_BUNDLE_MANIFEST_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
15
|
+
|
|
16
|
+
// Retry only transient Windows publication locks; never remove a winner.
|
|
17
|
+
function publishBundle(stage, root) {
|
|
18
|
+
const delays = [20, 40, 80, 160, 320];
|
|
19
|
+
for (let attempt = 0; ; attempt++) {
|
|
20
|
+
try {fs.renameSync(stage, root); return;} catch (error) {
|
|
21
|
+
if (process.platform !== 'win32' || attempt === delays.length ||
|
|
22
|
+
!['EBUSY', 'EPERM', 'EACCES'].includes(error.code) || fs.existsSync(root)) {throw error;}
|
|
23
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bundleError(code = 'AGENT_BUNDLE_INVALID') {return new CliError(t('agent.bundle_invalid'), { code });}
|
|
29
|
+
function slash(value) {return value.split(path.sep).join('/');}
|
|
30
|
+
|
|
31
|
+
function privateDirectory(directory) {
|
|
32
|
+
if (!path.isAbsolute(directory)) {throw bundleError();}
|
|
33
|
+
// Canonicalize an existing ancestor before creating anything. Verify every
|
|
34
|
+
// canonical parent against replacement by other OS users. A root-owned
|
|
35
|
+
// sticky temp directory is acceptable; our descendants must be private.
|
|
36
|
+
const suffix = [];
|
|
37
|
+
let ancestor = directory;
|
|
38
|
+
while (!fs.existsSync(ancestor)) {suffix.unshift(path.basename(ancestor)); ancestor = path.dirname(ancestor);}
|
|
39
|
+
if (fs.lstatSync(ancestor).isSymbolicLink()) {throw bundleError();}
|
|
40
|
+
let current = fs.realpathSync(ancestor);
|
|
41
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
42
|
+
for (let parent = current; ; parent = path.dirname(parent)) {
|
|
43
|
+
const stat = fs.lstatSync(parent);
|
|
44
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {throw bundleError();}
|
|
45
|
+
if (process.platform !== 'win32' && ((stat.uid !== uid && stat.uid !== 0) ||
|
|
46
|
+
(stat.mode & 0o022) && !(stat.uid === 0 && stat.mode & 0o1000))) {throw bundleError('AGENT_BUNDLE_UNSAFE_DIRECTORY');}
|
|
47
|
+
if (parent === path.dirname(parent)) {break;}
|
|
48
|
+
}
|
|
49
|
+
for (const component of suffix) {
|
|
50
|
+
current = path.join(current, component);
|
|
51
|
+
try {fs.mkdirSync(current, { mode: 0o700 });} catch (error) {if (error.code !== 'EEXIST') {throw error;}}
|
|
52
|
+
const stat = fs.lstatSync(current);
|
|
53
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== 'win32' && (stat.uid !== uid || stat.mode & 0o077)) {throw bundleError('AGENT_BUNDLE_UNSAFE_DIRECTORY');}
|
|
54
|
+
}
|
|
55
|
+
const final = fs.lstatSync(current);
|
|
56
|
+
if (process.platform !== 'win32' && (final.uid !== uid || final.mode & 0o077)) {throw bundleError('AGENT_BUNDLE_UNSAFE_DIRECTORY');}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packageMetadata(root) {
|
|
61
|
+
return parseStrictJson(readRegularFile(path.join(root, 'package.json'), 1024 * 1024).bytes, 1024 * 1024);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findDependency(root, name) {
|
|
65
|
+
if (!PACKAGE_NAME.test(name) || name.startsWith('.') || name.includes('..')) {throw bundleError();}
|
|
66
|
+
// Match normal node_modules ancestor resolution, without NODE_PATH, cwd, an
|
|
67
|
+
// npm subprocess, or an install hook. Preserve each package's own resolution.
|
|
68
|
+
for (let current = root; ; current = path.dirname(current)) {
|
|
69
|
+
const candidate = path.join(current, 'node_modules', name);
|
|
70
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) {return fs.realpathSync(candidate);}
|
|
71
|
+
if (current === path.dirname(current)) {return null;}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function walkFiles(root, visit, relative = '') {
|
|
76
|
+
const entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const next = path.join(relative, entry.name);
|
|
79
|
+
const stat = fs.lstatSync(path.join(root, next));
|
|
80
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && !stat.isFile()) {throw bundleError();}
|
|
81
|
+
if (stat.isDirectory()) {walkFiles(root, visit, next);} else {visit(slash(next), stat);}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function makeWritable(directory) {
|
|
86
|
+
if (!fs.existsSync(directory)) {return;}
|
|
87
|
+
fs.chmodSync(directory, 0o700);
|
|
88
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
89
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {makeWritable(path.join(directory, entry.name));}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sealDirectories(directory, sealRoot = true) {
|
|
94
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
95
|
+
if (entry.isDirectory()) {sealDirectories(path.join(directory, entry.name));}
|
|
96
|
+
}
|
|
97
|
+
if (sealRoot) {fs.chmodSync(directory, 0o500);}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function verifyBundle(root, expectedId) {
|
|
101
|
+
try {
|
|
102
|
+
if (fs.lstatSync(root).isSymbolicLink()) {throw bundleError();}
|
|
103
|
+
const raw = readRegularFile(path.join(root, 'bundle-manifest.json'), MAX_BUNDLE_MANIFEST_BYTES).bytes;
|
|
104
|
+
if (digest(raw) !== expectedId) {throw bundleError();}
|
|
105
|
+
const manifest = parseStrictJson(raw, MAX_BUNDLE_MANIFEST_BYTES);
|
|
106
|
+
if (manifest.schemaVersion !== 1 || manifest.product !== 'openyida-local-agent-bundle' || !Array.isArray(manifest.files) || manifest.files.length > MAX_FILES) {throw bundleError();}
|
|
107
|
+
const files = new Map();
|
|
108
|
+
for (const file of manifest.files) {
|
|
109
|
+
if (typeof file.path !== 'string' || file.path.startsWith('/') || file.path.includes('\\') ||
|
|
110
|
+
file.path.split('/').some((part) => !part || part === '.' || part === '..') || files.has(file.path) ||
|
|
111
|
+
!Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_FILE_BYTES || !/^[a-f0-9]{64}$/.test(file.sha256) ||
|
|
112
|
+
![0o444, 0o555].includes(file.mode)) {throw bundleError();}
|
|
113
|
+
files.set(file.path, file);
|
|
114
|
+
}
|
|
115
|
+
const actual = new Set();
|
|
116
|
+
walkFiles(root, (relative, stat) => {
|
|
117
|
+
if (relative === 'bundle-manifest.json') {return;}
|
|
118
|
+
const file = files.get(relative);
|
|
119
|
+
if (!file || stat.size !== file.size || process.platform !== 'win32' && (stat.mode & 0o777) !== file.mode ||
|
|
120
|
+
digest(readRegularFile(path.join(root, relative), MAX_FILE_BYTES).bytes) !== file.sha256) {throw bundleError();}
|
|
121
|
+
actual.add(relative);
|
|
122
|
+
});
|
|
123
|
+
if (actual.size !== files.size) {throw bundleError();}
|
|
124
|
+
return manifest;
|
|
125
|
+
} catch {throw bundleError();}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function materializeBundle(runtime, config, options = {}) {
|
|
129
|
+
const sourceRoot = fs.realpathSync(config.node.packageRoot);
|
|
130
|
+
const cache = privateDirectory(options.bundleCache || path.join(config.stateDir, 'bundles'));
|
|
131
|
+
let stage;
|
|
132
|
+
const pendingFiles = [];
|
|
133
|
+
const files = [];
|
|
134
|
+
const sources = [];
|
|
135
|
+
const sourceTrees = [];
|
|
136
|
+
const destinations = new Set();
|
|
137
|
+
let totalBytes = 0;
|
|
138
|
+
let published = false;
|
|
139
|
+
let runtimePackage;
|
|
140
|
+
const nodeBytes = readRegularFile(config.node.executable, MAX_FILE_BYTES).bytes;
|
|
141
|
+
const nodeIdentity = { version: process.version, sha256: digest(nodeBytes) };
|
|
142
|
+
function writeFile(relative, bytes, executable = false, source) {
|
|
143
|
+
if (destinations.has(relative) || files.length >= MAX_FILES || (totalBytes += bytes.length) > MAX_BUNDLE_BYTES) {throw bundleError();}
|
|
144
|
+
destinations.add(relative);
|
|
145
|
+
const mode = executable ? 0o555 : 0o444;
|
|
146
|
+
const sha256 = digest(bytes);
|
|
147
|
+
files.push({ path: relative, size: bytes.length, sha256, mode });
|
|
148
|
+
// Inventory first. Reconnecting to the same content needs no staging copy.
|
|
149
|
+
// Retain paths instead of whole dependency trees in memory; new bundles
|
|
150
|
+
// reread and check each file against this snapshot before writing it.
|
|
151
|
+
pendingFiles.push({ relative, source, bytes: source ? undefined : bytes, mode, sha256 });
|
|
152
|
+
}
|
|
153
|
+
function copyFile(source, target) {
|
|
154
|
+
const file = readRegularFile(source, MAX_FILE_BYTES);
|
|
155
|
+
writeFile(target, file.bytes, Boolean(file.stat.mode & 0o111), source);
|
|
156
|
+
sources.push({ path: source, sha256: digest(file.bytes), mode: file.stat.mode & 0o111 });
|
|
157
|
+
}
|
|
158
|
+
function copyTree(source, target, skip = () => false) {
|
|
159
|
+
const stat = fs.lstatSync(source);
|
|
160
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {throw bundleError();}
|
|
161
|
+
const treeFiles = [];
|
|
162
|
+
function copy(relative = '') {
|
|
163
|
+
for (const entry of fs.readdirSync(path.join(source, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
|
|
164
|
+
const next = path.join(relative, entry.name);
|
|
165
|
+
if (skip(slash(next), entry)) {continue;}
|
|
166
|
+
const filename = path.join(source, next);
|
|
167
|
+
const child = fs.lstatSync(filename);
|
|
168
|
+
if (child.isSymbolicLink() || !child.isFile() && !child.isDirectory()) {throw bundleError();}
|
|
169
|
+
if (child.isDirectory()) {copy(next);} else {treeFiles.push(slash(next)); copyFile(filename, `${target}/${slash(next)}`);}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
copy();
|
|
173
|
+
sourceTrees.push({ source, skip, files: treeFiles.sort() });
|
|
174
|
+
}
|
|
175
|
+
function copyDependencies(source, target, ancestors = []) {
|
|
176
|
+
if (ancestors.length > 50) {throw bundleError();}
|
|
177
|
+
const metadata = packageMetadata(source);
|
|
178
|
+
const optional = metadata.optionalDependencies || {};
|
|
179
|
+
const peers = metadata.peerDependencies || {};
|
|
180
|
+
const dependencies = { ...peers, ...metadata.dependencies, ...optional };
|
|
181
|
+
for (const name of Object.keys(dependencies).sort()) {
|
|
182
|
+
// Runtime is copied from the verified bytes, never from optional node_modules.
|
|
183
|
+
if (source === sourceRoot && name === runtimePackage) {continue;}
|
|
184
|
+
const resolved = findDependency(source, name);
|
|
185
|
+
if (!resolved) {
|
|
186
|
+
if (Object.hasOwn(optional, name) || metadata.peerDependenciesMeta?.[name]?.optional) {continue;}
|
|
187
|
+
throw bundleError('AGENT_BUNDLE_DEPENDENCY_MISSING');
|
|
188
|
+
}
|
|
189
|
+
if (packageMetadata(resolved).name !== name) {throw bundleError();}
|
|
190
|
+
// An ancestor copy already satisfies cycles using normal Node resolution.
|
|
191
|
+
if (ancestors.includes(resolved)) {continue;}
|
|
192
|
+
const destination = `${target}/node_modules/${name}`;
|
|
193
|
+
copyTree(resolved, destination, (relative) => ['node_modules', '.git', '.cache'].includes(relative.split('/')[0]));
|
|
194
|
+
copyDependencies(resolved, destination, [...ancestors, resolved]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const metadata = packageMetadata(sourceRoot);
|
|
199
|
+
if (metadata.name !== 'openyida' || typeof metadata.version !== 'string') {throw bundleError();}
|
|
200
|
+
// Explicit development runtimes may run on CI platforms without a public
|
|
201
|
+
// runtime package. Keep the production gate and supported-package exclusion.
|
|
202
|
+
runtimePackage = runtime.development === true && !PRODUCTION_RUNTIME_PLATFORMS.includes(process.platform)
|
|
203
|
+
? undefined : platformPackageName(process.platform, process.arch);
|
|
204
|
+
// Deliberate installed CLI allowlist: no checkout, private state, user
|
|
205
|
+
// project, devDependencies, postinstall execution, or global Skills links.
|
|
206
|
+
for (const directory of ['bin', 'lib', 'yida-skills']) {
|
|
207
|
+
copyTree(path.join(sourceRoot, directory), `openyida/${directory}`, (relative) => directory === 'lib' && /^samples\/.+\/assets(?:\/|$)/.test(relative));
|
|
208
|
+
}
|
|
209
|
+
for (const filename of ['package.json', 'LICENSE', 'README.md', 'project/config.json']) {
|
|
210
|
+
if (fs.existsSync(path.join(sourceRoot, filename))) {copyFile(path.join(sourceRoot, filename), `openyida/${filename}`);}
|
|
211
|
+
}
|
|
212
|
+
const prd = path.join(sourceRoot, 'project', 'prd');
|
|
213
|
+
if (fs.existsSync(prd)) {
|
|
214
|
+
for (const name of fs.readdirSync(prd).sort()) {if (/^demo-.*\.md$/.test(name)) {copyFile(path.join(prd, name), `openyida/project/prd/${name}`);}}
|
|
215
|
+
}
|
|
216
|
+
copyDependencies(sourceRoot, 'openyida', [sourceRoot]);
|
|
217
|
+
// An npm replacement during copying must not produce an internally mixed
|
|
218
|
+
// CLI/skills snapshot. Recheck source content and tree membership before
|
|
219
|
+
// publishing. Once published, execution never consults these source paths.
|
|
220
|
+
const verifySources = () => {
|
|
221
|
+
for (const source of sources) {
|
|
222
|
+
const current = readRegularFile(source.path, MAX_FILE_BYTES);
|
|
223
|
+
if (digest(current.bytes) !== source.sha256 || (current.stat.mode & 0o111) !== source.mode) {throw bundleError('AGENT_BUNDLE_SOURCE_CHANGED');}
|
|
224
|
+
}
|
|
225
|
+
for (const tree of sourceTrees) {
|
|
226
|
+
const actual = [];
|
|
227
|
+
const inspect = (relative = '') => {
|
|
228
|
+
for (const entry of fs.readdirSync(path.join(tree.source, relative), { withFileTypes: true })) {
|
|
229
|
+
const next = slash(path.join(relative, entry.name));
|
|
230
|
+
if (tree.skip(next, entry)) {continue;}
|
|
231
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) {throw bundleError();}
|
|
232
|
+
if (entry.isDirectory()) {inspect(next);} else {actual.push(next);}
|
|
233
|
+
if (actual.length > MAX_FILES) {throw bundleError();}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
inspect();
|
|
237
|
+
if (JSON.stringify(actual.sort()) !== JSON.stringify(tree.files)) {throw bundleError('AGENT_BUNDLE_SOURCE_CHANGED');}
|
|
238
|
+
}
|
|
239
|
+
if (JSON.stringify(packageMetadata(sourceRoot)) !== JSON.stringify(metadata)) {throw bundleError('AGENT_BUNDLE_SOURCE_CHANGED');}
|
|
240
|
+
};
|
|
241
|
+
const runtimeFile = `runtime/bin/openyida-agent-runtime${process.platform === 'win32' ? '.exe' : ''}`;
|
|
242
|
+
if (digest(runtime.binaryBytes) !== runtime.sha256) {throw bundleError();}
|
|
243
|
+
writeFile(runtimeFile, runtime.binaryBytes, true);
|
|
244
|
+
writeFile('runtime/runtime-manifest.json', runtime.manifestBytes);
|
|
245
|
+
if (runtime.signatureBytes) {writeFile('runtime/runtime-manifest.sig', runtime.signatureBytes);}
|
|
246
|
+
const manifest = {
|
|
247
|
+
schemaVersion: 1, product: 'openyida-local-agent-bundle', openyidaVersion: metadata.version,
|
|
248
|
+
runtimeVersion: runtime.runtimeVersion, launcherProtocol: 1, wireProtocol: 1, node: nodeIdentity,
|
|
249
|
+
runtime: { executable: runtimeFile, manifest: 'runtime/runtime-manifest.json', signature: runtime.signatureBytes ? 'runtime/runtime-manifest.sig' : null, development: runtime.development === true },
|
|
250
|
+
cliEntry: 'openyida/bin/yida.js', skillsDir: 'openyida/yida-skills',
|
|
251
|
+
files: files.sort((a, b) => Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8'))),
|
|
252
|
+
};
|
|
253
|
+
if (!destinations.has(manifest.cliEntry) || !files.some((file) => file.path.startsWith(`${manifest.skillsDir}/`))) {throw bundleError();}
|
|
254
|
+
const raw = Buffer.from(`${JSON.stringify(manifest)}\n`);
|
|
255
|
+
if (raw.length > MAX_BUNDLE_MANIFEST_BYTES) {throw bundleError();}
|
|
256
|
+
const id = digest(raw);
|
|
257
|
+
const root = path.join(cache, id);
|
|
258
|
+
if (!fs.existsSync(root)) {
|
|
259
|
+
stage = fs.mkdtempSync(path.join(cache, '.staging-'));
|
|
260
|
+
fs.chmodSync(stage, 0o700);
|
|
261
|
+
for (const file of pendingFiles) {
|
|
262
|
+
const bytes = file.source ? readRegularFile(file.source, MAX_FILE_BYTES).bytes : file.bytes;
|
|
263
|
+
if (digest(bytes) !== file.sha256) {throw bundleError('AGENT_BUNDLE_SOURCE_CHANGED');}
|
|
264
|
+
const target = path.join(stage, file.relative);
|
|
265
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
266
|
+
fs.writeFileSync(target, bytes, { flag: 'wx', mode: file.mode });
|
|
267
|
+
fs.chmodSync(target, file.mode);
|
|
268
|
+
}
|
|
269
|
+
verifySources();
|
|
270
|
+
fs.writeFileSync(path.join(stage, 'bundle-manifest.json'), raw, { flag: 'wx', mode: 0o444 });
|
|
271
|
+
fs.chmodSync(path.join(stage, 'bundle-manifest.json'), 0o444);
|
|
272
|
+
verifyBundle(stage, id);
|
|
273
|
+
sealDirectories(stage, false);
|
|
274
|
+
try {publishBundle(stage, root); published = true;}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EACCES', 'EPERM'].includes(error.code) || !fs.existsSync(root)) {throw error;}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// Never trust a version or mtime cache. Check source stability and every
|
|
280
|
+
// byte of the existing bundle, including unexpected entries, on reuse.
|
|
281
|
+
verifySources();
|
|
282
|
+
}
|
|
283
|
+
if (published) {fs.chmodSync(root, 0o500);}
|
|
284
|
+
verifyBundle(root, id);
|
|
285
|
+
const bundle = { schemaVersion: 1, id, root, manifestPath: path.join(root, 'bundle-manifest.json'), manifestSha256: id,
|
|
286
|
+
runtimeVersion: runtime.runtimeVersion, skillsDir: path.join(root, manifest.skillsDir) };
|
|
287
|
+
return { runtime: { ...runtime, executable: path.join(root, runtimeFile), manifestPath: path.join(root, 'runtime/runtime-manifest.json') },
|
|
288
|
+
config: { ...config, bundle, node: { ...config.node, packageRoot: path.join(root, 'openyida'), cliEntry: path.join(root, manifest.cliEntry),
|
|
289
|
+
version: metadata.version, nodeVersion: nodeIdentity.version, sha256: nodeIdentity.sha256 } } };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error instanceof CliError) {throw error;}
|
|
292
|
+
throw bundleError();
|
|
293
|
+
} finally {
|
|
294
|
+
if (stage && !published) {makeWritable(stage); fs.rmSync(stage, { recursive: true, force: true });}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = { materializeBundle, verifyBundle, privateDirectory };
|