@miruzero/zero-agent 0.3.6 → 0.3.7
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 +31 -22
- package/package.json +6 -3
- package/src/agent.js +193 -167
- package/src/args.js +1 -3
- package/src/autostart.js +44 -68
- package/src/capabilities.js +95 -138
- package/src/chatgpt/auth/index.js +114 -0
- package/src/chatgpt/config/paths.js +25 -0
- package/src/chatgpt/index.js +5 -0
- package/src/chatgpt/session/direct-transport.js +68 -0
- package/src/chatgpt/session/exchange.js +17 -0
- package/src/chatgpt/session/index.js +102 -0
- package/src/chatgpt/session/local-storage.js +106 -0
- package/src/chatgpt/transport/direct/http-client.js +191 -0
- package/src/chatgpt-auth.js +17 -111
- package/src/chatgpt-runtime.js +8 -0
- package/src/chatgpt-session-exchange.js +8 -0
- package/src/chatgpt-session.js +26 -0
- package/src/cli.js +108 -71
- package/src/command/exec.js +34 -0
- package/src/command/filesystem/append-file.js +7 -12
- package/src/command/filesystem/copy-directory.js +8 -12
- package/src/command/filesystem/copy-file.js +8 -13
- package/src/command/filesystem/create-directory.js +7 -11
- package/src/command/filesystem/create-file.js +8 -13
- package/src/command/filesystem/delete-directory.js +7 -11
- package/src/command/filesystem/delete-file.js +8 -12
- package/src/command/filesystem/exists.js +6 -10
- package/src/command/filesystem/get-file-info.js +7 -12
- package/src/command/filesystem/glob-files.js +8 -13
- package/src/command/filesystem/grep-files.js +9 -15
- package/src/command/filesystem/hash-file.js +6 -11
- package/src/command/filesystem/index.js +33 -34
- package/src/command/filesystem/list-directory.js +6 -10
- package/src/command/filesystem/move-directory.js +9 -14
- package/src/command/filesystem/move-file.js +8 -13
- package/src/command/filesystem/path-policy.js +33 -16
- package/src/command/filesystem/read-file.js +7 -11
- package/src/command/filesystem/read-files.js +5 -8
- package/src/command/filesystem/rename-file.js +5 -8
- package/src/command/filesystem/replace-file.js +7 -12
- package/src/command/filesystem/text.js +5 -7
- package/src/command/filesystem/touch-file.js +8 -13
- package/src/command/filesystem/truncate-file.js +6 -10
- package/src/command/filesystem/walk.js +36 -39
- package/src/command/filesystem/write-file.js +8 -13
- package/src/command/index.js +19 -24
- package/src/command/path/index.js +46 -53
- package/src/config.js +21 -33
- package/src/device-paths.js +53 -0
- package/src/enrollment-client.js +19 -0
- package/src/http.js +27 -2
- package/src/identity-state.js +27 -0
- package/src/index.js +2 -4
- package/src/instance.js +85 -0
- package/src/local-server.js +174 -0
- package/src/local-tools.js +205 -205
- package/src/shared-zero/catalog.js +33 -0
- package/src/shared-zero/contract.js +18 -0
- package/src/shared-zero/descriptor.js +25 -0
- package/src/shared-zero/errors.js +12 -0
- package/src/shared-zero/index.js +9 -0
- package/src/shared-zero/runtime.js +14 -0
- package/src/shared-zero/target.js +19 -0
- package/src/shared-zero/tool-id.js +26 -0
- package/src/storage-roots.js +21 -0
- package/src/zero-runtime.js +1 -11
- package/src/zero.js +78 -0
|
@@ -1,39 +1,36 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const stat = fs.statSync(fullPath);
|
|
8
|
-
return {
|
|
9
|
-
path: fullPath,
|
|
10
|
-
relativePath: path.relative(root, fullPath).replace(/\\/g, '/'),
|
|
11
|
-
type: stat.isDirectory() ? 'directory' : 'file',
|
|
12
|
-
size: stat.size
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const depth = options.depth ?? Infinity;
|
|
18
|
-
const filesOnly = options.filesOnly === true;
|
|
19
|
-
const out = [];
|
|
20
|
-
const visit = (dir, level) => {
|
|
21
|
-
if (level > depth) return;
|
|
22
|
-
for (const name of fs.readdirSync(dir)) {
|
|
23
|
-
const full = path.join(dir, name);
|
|
24
|
-
const item = entry(full, root);
|
|
25
|
-
if (!filesOnly || item.type === 'file') out.push(item);
|
|
26
|
-
if (item.type === 'directory') visit(full, level + 1);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
visit(root, 1);
|
|
30
|
-
return out;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
35
|
-
.replace(/\*/g, '.*')
|
|
36
|
-
.replace(/\?/g, '.') + '$', 'i');
|
|
37
|
-
|
|
38
|
-
const walkModule = { entry, walk, wildcard };
|
|
39
|
-
Object.assign(exports, walkModule);
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
exports.entry = (fullPath, root) => {
|
|
7
|
+
const stat = zero.fs.statSync(fullPath);
|
|
8
|
+
return {
|
|
9
|
+
path: fullPath,
|
|
10
|
+
relativePath: zero.path.relative(root, fullPath).replace(/\\/g, '/'),
|
|
11
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
12
|
+
size: stat.size
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
exports.walk = (root, options = {}) => {
|
|
17
|
+
const depth = options.depth ?? Infinity;
|
|
18
|
+
const filesOnly = options.filesOnly === true;
|
|
19
|
+
const out = [];
|
|
20
|
+
const visit = (dir, level) => {
|
|
21
|
+
if (level > depth) return;
|
|
22
|
+
for (const name of zero.fs.readdirSync(dir)) {
|
|
23
|
+
const full = zero.path.join(dir, name);
|
|
24
|
+
const item = exports.entry(full, root);
|
|
25
|
+
if (!filesOnly || item.type === 'file') out.push(item);
|
|
26
|
+
if (item.type === 'directory') visit(full, level + 1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
visit(root, 1);
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.wildcard = (pattern) => new RegExp('^' + String(pattern)
|
|
34
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
35
|
+
.replace(/\*/g, '.*')
|
|
36
|
+
.replace(/\?/g, '.') + '$', 'i');
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const zero = require('../../zero-runtime');
|
|
6
|
-
const pathPolicy = require('./path-policy');
|
|
3
|
+
const zero = require('../../zero');
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
|
|
6
|
+
exports.descriptor = zero.descriptor.validate({
|
|
9
7
|
name: 'zero.command.filesystem.writeFile',
|
|
10
8
|
aliases: ['zero.command.writeFile'],
|
|
11
9
|
description: 'Write or append text content to a file inside allowed roots.',
|
|
@@ -25,15 +23,12 @@ const descriptor = zero.descriptor.validate({
|
|
|
25
23
|
}
|
|
26
24
|
});
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
26
|
+
exports.writeFile = (args = {}, context = {}) => {
|
|
27
|
+
const file = zero.pathPolicy.resolveAllowed(args.path, context);
|
|
30
28
|
const mode = args.mode === 'append' ? 'append' : 'overwrite';
|
|
31
29
|
const data = String(args.content ?? '');
|
|
32
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
|
-
if (mode === 'append') fs.appendFileSync(file, data, 'utf8');
|
|
34
|
-
else fs.writeFileSync(file, data, 'utf8');
|
|
30
|
+
zero.fs.mkdirSync(zero.path.dirname(file), { recursive: true });
|
|
31
|
+
if (mode === 'append') zero.fs.appendFileSync(file, data, 'utf8');
|
|
32
|
+
else zero.fs.writeFileSync(file, data, 'utf8');
|
|
35
33
|
return { ok: true, path: file, mode, bytes: Buffer.byteLength(data) };
|
|
36
34
|
};
|
|
37
|
-
|
|
38
|
-
const writeFileModule = { descriptor, writeFile };
|
|
39
|
-
Object.assign(exports, writeFileModule);
|
package/src/command/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const zero = require('../zero
|
|
4
|
-
|
|
5
|
-
const pathTools = require('./path');
|
|
3
|
+
const zero = require('../zero');
|
|
4
|
+
|
|
6
5
|
|
|
7
6
|
const fsToolNames = [
|
|
8
7
|
'readFile', 'readFiles', 'writeFile', 'appendFile', 'createFile', 'touchFile', 'replaceFile',
|
|
@@ -12,38 +11,34 @@ const fsToolNames = [
|
|
|
12
11
|
const pathToolNames = [
|
|
13
12
|
'resolvePath', 'normalizePath', 'joinPath', 'relativePath', 'getParentDirectory', 'getFilename', 'getExtension'
|
|
14
13
|
];
|
|
14
|
+
const execToolNames = ['exec'];
|
|
15
15
|
|
|
16
16
|
const descriptors = [
|
|
17
|
-
...fsToolNames.map((name) =>
|
|
18
|
-
...pathToolNames.map((name) =>
|
|
17
|
+
...fsToolNames.map((name) => zero.commandFilesystem[`${name}Descriptor`]),
|
|
18
|
+
...pathToolNames.map((name) => zero.commandPath[`${name}Descriptor`]),
|
|
19
|
+
...execToolNames.map((name) => zero.commandExec[`${name}Descriptor`])
|
|
19
20
|
];
|
|
20
21
|
const catalog = zero.catalog.create(descriptors);
|
|
21
22
|
const handlers = new Map([
|
|
22
|
-
...fsToolNames.map((name) => [
|
|
23
|
-
...pathToolNames.map((name) => [
|
|
23
|
+
...fsToolNames.map((name) => [zero.commandFilesystem[`${name}Descriptor`].name, zero.commandFilesystem[name]]),
|
|
24
|
+
...pathToolNames.map((name) => [zero.commandPath[`${name}Descriptor`].name, zero.commandPath[name]]),
|
|
25
|
+
...execToolNames.map((name) => [zero.commandExec[`${name}Descriptor`].name, zero.commandExec[name]])
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
28
|
const localAliasMap = {};
|
|
27
|
-
for (const name of fsToolNames) {
|
|
28
|
-
localAliasMap[name] = `zero.command.${name}`;
|
|
29
|
-
localAliasMap[`command.${name}`] = `zero.command.${name}`;
|
|
30
|
-
}
|
|
31
|
-
for (const name of pathToolNames) {
|
|
29
|
+
for (const name of [...fsToolNames, ...pathToolNames, ...execToolNames]) {
|
|
32
30
|
localAliasMap[name] = `zero.command.${name}`;
|
|
33
31
|
localAliasMap[`command.${name}`] = `zero.command.${name}`;
|
|
34
32
|
}
|
|
35
33
|
const localAlias = (name) => localAliasMap[name] || name;
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
35
|
+
exports.filesystem = Object.fromEntries(fsToolNames.map((name) => [name, zero.commandFilesystem[name]]));
|
|
36
|
+
exports.path = Object.fromEntries(pathToolNames.map((name) => [name, zero.commandPath[name]]));
|
|
37
|
+
exports.exec = Object.fromEntries(execToolNames.map((name) => [name, zero.commandExec[name]]));
|
|
38
|
+
exports.listTools = (filter = {}) => catalog.list(filter);
|
|
39
|
+
exports.resolveTool = (name) => catalog.resolve(localAlias(name));
|
|
40
|
+
exports.callTool = (name, args = {}, context = {}) => {
|
|
41
|
+
const descriptor = catalog.resolve(localAlias(name));
|
|
42
|
+
const handler = handlers.get(descriptor.name);
|
|
43
|
+
return handler(args, context);
|
|
47
44
|
};
|
|
48
|
-
|
|
49
|
-
Object.assign(exports, command);
|
|
@@ -1,53 +1,46 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const descriptor = (name, description, properties, required = []) => zero.descriptor.validate({
|
|
7
|
-
name: `zero.command.path.${name}`,
|
|
8
|
-
aliases: [`zero.command.${name}`],
|
|
9
|
-
description,
|
|
10
|
-
capability: 'path.compute',
|
|
11
|
-
risk: 'read',
|
|
12
|
-
execution: 'target',
|
|
13
|
-
adapters: ['native'],
|
|
14
|
-
inputSchema: { type: 'object', properties, required, additionalProperties: false }
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
path: { type: 'string' }, base: { type: 'string' }
|
|
19
|
-
}, ['path']);
|
|
20
|
-
|
|
21
|
-
path: { type: 'string' }
|
|
22
|
-
}, ['path']);
|
|
23
|
-
|
|
24
|
-
segments: { type: 'array', items: { type: 'string' }, minItems: 1 }
|
|
25
|
-
}, ['segments']);
|
|
26
|
-
|
|
27
|
-
from: { type: 'string' }, to: { type: 'string' }
|
|
28
|
-
}, ['from', 'to']);
|
|
29
|
-
|
|
30
|
-
path: { type: 'string' }
|
|
31
|
-
}, ['path']);
|
|
32
|
-
|
|
33
|
-
path: { type: 'string' }, stripExtension: { type: 'boolean', default: false }
|
|
34
|
-
}, ['path']);
|
|
35
|
-
|
|
36
|
-
path: { type: 'string' }
|
|
37
|
-
}, ['path']);
|
|
38
|
-
|
|
39
|
-
const asText = (value) => String(value ?? '');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const pathTools = {
|
|
49
|
-
resolvePathDescriptor, normalizePathDescriptor, joinPathDescriptor, relativePathDescriptor,
|
|
50
|
-
getParentDirectoryDescriptor, getFilenameDescriptor, getExtensionDescriptor,
|
|
51
|
-
resolvePath, normalizePath, joinPath, relativePath, getParentDirectory, getFilename, getExtension
|
|
52
|
-
};
|
|
53
|
-
Object.assign(exports, pathTools);
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const descriptor = (name, description, properties, required = []) => zero.descriptor.validate({
|
|
7
|
+
name: `zero.command.path.${name}`,
|
|
8
|
+
aliases: [`zero.command.${name}`],
|
|
9
|
+
description,
|
|
10
|
+
capability: 'zero.path.compute',
|
|
11
|
+
risk: 'read',
|
|
12
|
+
execution: 'target',
|
|
13
|
+
adapters: ['native'],
|
|
14
|
+
inputSchema: { type: 'object', properties, required, additionalProperties: false }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
exports.resolvePathDescriptor = descriptor('resolvePath', 'Resolve a path against an optional base zero.path.', {
|
|
18
|
+
path: { type: 'string' }, base: { type: 'string' }
|
|
19
|
+
}, ['path']);
|
|
20
|
+
exports.normalizePathDescriptor = descriptor('normalizePath', 'Normalize a path string for the target platform.', {
|
|
21
|
+
path: { type: 'string' }
|
|
22
|
+
}, ['path']);
|
|
23
|
+
exports.joinPathDescriptor = descriptor('joinPath', 'Join path segments for the target platform.', {
|
|
24
|
+
segments: { type: 'array', items: { type: 'string' }, minItems: 1 }
|
|
25
|
+
}, ['segments']);
|
|
26
|
+
exports.relativePathDescriptor = descriptor('relativePath', 'Compute a relative path between two paths.', {
|
|
27
|
+
from: { type: 'string' }, to: { type: 'string' }
|
|
28
|
+
}, ['from', 'to']);
|
|
29
|
+
exports.getParentDirectoryDescriptor = descriptor('getParentDirectory', 'Return the parent directory name for a zero.path.', {
|
|
30
|
+
path: { type: 'string' }
|
|
31
|
+
}, ['path']);
|
|
32
|
+
exports.getFilenameDescriptor = descriptor('getFilename', 'Return the filename portion of a zero.path.', {
|
|
33
|
+
path: { type: 'string' }, stripExtension: { type: 'boolean', default: false }
|
|
34
|
+
}, ['path']);
|
|
35
|
+
exports.getExtensionDescriptor = descriptor('getExtension', 'Return the extension portion of a zero.path.', {
|
|
36
|
+
path: { type: 'string' }
|
|
37
|
+
}, ['path']);
|
|
38
|
+
|
|
39
|
+
const asText = (value) => String(value ?? '');
|
|
40
|
+
exports.resolvePath = (args = {}) => ({ ok: true, path: args.base ? zero.path.resolve(asText(args.base), asText(args.path)) : zero.path.resolve(asText(args.path)) });
|
|
41
|
+
exports.normalizePath = (args = {}) => ({ ok: true, path: zero.path.normalize(asText(args.path)) });
|
|
42
|
+
exports.joinPath = (args = {}) => ({ ok: true, path: zero.path.join(...(Array.isArray(args.segments) ? args.segments.map(asText) : [])) });
|
|
43
|
+
exports.relativePath = (args = {}) => ({ ok: true, path: zero.path.relative(asText(args.from), asText(args.to)) });
|
|
44
|
+
exports.getParentDirectory = (args = {}) => ({ ok: true, path: zero.path.dirname(asText(args.path)) });
|
|
45
|
+
exports.getFilename = (args = {}) => ({ ok: true, name: zero.path.basename(asText(args.path), args.stripExtension ? zero.path.extname(asText(args.path)) : undefined) });
|
|
46
|
+
exports.getExtension = (args = {}) => ({ ok: true, extension: zero.path.extname(asText(args.path)) });
|
package/src/config.js
CHANGED
|
@@ -1,52 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
3
|
+
const zero = require('./zero');
|
|
6
4
|
|
|
7
|
-
const defaultHome = () => path.join(os.homedir(), '.zero-agent');
|
|
8
5
|
|
|
9
|
-
const
|
|
10
|
-
const deviceFile = (env = process.env) => path.join(agentHome(env), 'device.json');
|
|
11
|
-
const logDir = (env = process.env) => path.join(agentHome(env), 'logs');
|
|
6
|
+
const defaultHome = () => zero.path.join(zero.os.homedir(), '.zero-agent');
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
exports.agentHome = (env = process.env) => zero.path.resolve(env.ZERO_AGENT_HOME || defaultHome());
|
|
9
|
+
exports.deviceFile = (env = process.env) => zero.path.join(exports.agentHome(env), 'device.json');
|
|
10
|
+
exports.logDir = (env = process.env) => zero.path.join(exports.agentHome(env), 'logs');
|
|
11
|
+
|
|
12
|
+
exports.ensureDirs = (env = process.env) => {
|
|
13
|
+
zero.fs.mkdirSync(exports.agentHome(env), { recursive: true });
|
|
14
|
+
zero.fs.mkdirSync(exports.logDir(env), { recursive: true });
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
if (!fs.existsSync(file)) return fallback;
|
|
20
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
exports.readJson = (file, fallback = null) => {
|
|
18
|
+
if (!zero.fs.existsSync(file)) return fallback;
|
|
19
|
+
return JSON.parse(zero.fs.readFileSync(file, 'utf8'));
|
|
21
20
|
};
|
|
22
|
-
|
|
23
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
24
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
21
|
+
exports.writeJson = (file, value) => {
|
|
22
|
+
zero.fs.mkdirSync(zero.path.dirname(file), { recursive: true });
|
|
23
|
+
zero.fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
exports.loadDevice = (env = process.env) => exports.readJson(exports.deviceFile(env), null);
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
ensureDirs(env);
|
|
31
|
-
writeJson(deviceFile(env), device);
|
|
32
|
-
return deviceFile(env);
|
|
28
|
+
exports.saveDevice = (device, env = process.env) => {
|
|
29
|
+
exports.ensureDirs(env);
|
|
30
|
+
exports.writeJson(exports.deviceFile(env), device);
|
|
31
|
+
return exports.deviceFile(env);
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
exports.redactedDevice = (device) => {
|
|
36
35
|
if (!device) return null;
|
|
37
36
|
const clone = { ...device };
|
|
38
37
|
if (clone.device_token) clone.device_token = 'REDACTED';
|
|
39
38
|
if (clone.token) clone.token = 'REDACTED';
|
|
40
39
|
return clone;
|
|
41
40
|
};
|
|
42
|
-
module.exports = {
|
|
43
|
-
agentHome,
|
|
44
|
-
deviceFile,
|
|
45
|
-
logDir,
|
|
46
|
-
ensureDirs,
|
|
47
|
-
readJson,
|
|
48
|
-
writeJson,
|
|
49
|
-
loadDevice,
|
|
50
|
-
saveDevice,
|
|
51
|
-
redactedDevice
|
|
52
|
-
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
exports.storeFile = (env = process.env) => zero.path.resolve(
|
|
6
|
+
env.ZERO_DEVICE_PATH_STORE || zero.path.join(zero.os.homedir(), '.zero', 'device-paths.json')
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
exports.normalizeRoots = (roots = []) => [...new Set(
|
|
10
|
+
roots.filter((item) => typeof item === 'string' && item.trim()).map((item) => zero.path.resolve(item.trim()))
|
|
11
|
+
)];
|
|
12
|
+
|
|
13
|
+
exports.saveAllowedRoots = (roots = [], env = process.env) => {
|
|
14
|
+
const allowed_roots = exports.normalizeRoots(roots);
|
|
15
|
+
const file = exports.storeFile(env);
|
|
16
|
+
const state = { allowed_roots, updated_at: new Date().toISOString() };
|
|
17
|
+
zero.fs.mkdirSync(zero.path.dirname(file), { recursive: true });
|
|
18
|
+
const temp = file + '.tmp-' + process.pid + '-' + Date.now();
|
|
19
|
+
zero.fs.writeFileSync(temp, JSON.stringify(state, null, 2));
|
|
20
|
+
zero.fs.renameSync(temp, file);
|
|
21
|
+
return { ok: true, store: file, ...state };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const defaultListDirectories = (root) => zero.fs.readdirSync(root, { withFileTypes: true })
|
|
25
|
+
.filter((item) => item.isDirectory())
|
|
26
|
+
.slice(0, 80)
|
|
27
|
+
.map((item) => item.name);
|
|
28
|
+
|
|
29
|
+
exports.snapshot = (env = process.env, system = {}) => {
|
|
30
|
+
const platform = system.platform || process.platform;
|
|
31
|
+
const home = system.home || zero.os.homedir();
|
|
32
|
+
const cwd = system.cwd || process.cwd();
|
|
33
|
+
const exists = system.exists || zero.fs.existsSync;
|
|
34
|
+
const listDirectories = system.listDirectories || defaultListDirectories;
|
|
35
|
+
const drives = zero.storageRoots.discover({ platform, exists });
|
|
36
|
+
const locations = [];
|
|
37
|
+
for (const drive of drives) {
|
|
38
|
+
try {
|
|
39
|
+
for (const name of listDirectories(drive)) {
|
|
40
|
+
locations.push(platform === 'win32' ? zero.path.win32.join(drive, name) : zero.path.posix.join(drive, name));
|
|
41
|
+
if (locations.length >= 160) break;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
if (locations.length >= 160) break;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
platform, home, cwd, drives, storage_roots: drives, locations,
|
|
48
|
+
allowed_roots: zero.pathPolicy.rootsFor({ storageRoots: drives }, env),
|
|
49
|
+
source: 'agent',
|
|
50
|
+
discovered_at: new Date().toISOString()
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const buildClaimPayload = (ticket, env = process.env) => ({
|
|
7
|
+
ticket_id: ticket,
|
|
8
|
+
device_id: zero.identityState.getDeviceId(env),
|
|
9
|
+
agent_id: zero.identityState.getAgentId(env)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const buildCompletionPayload = (ticket, env = process.env) => ({
|
|
13
|
+
ticket_id: ticket,
|
|
14
|
+
device_id: zero.identityState.getDeviceId(env),
|
|
15
|
+
identity_id: zero.identityState.getIdentityId(env)
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
exports.buildClaimPayload = buildClaimPayload;
|
|
19
|
+
exports.buildCompletionPayload = buildCompletionPayload;
|
package/src/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
exports.requestJson = async (url, { method = 'GET', token = null, body = null, timeoutMs = 15000 } = {}) => {
|
|
4
4
|
const controller = new AbortController();
|
|
5
5
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
6
6
|
try {
|
|
@@ -22,4 +22,29 @@ const requestJson = async (url, { method = 'GET', token = null, body = null, tim
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
exports.consumeEventStream = async (url, { token = null, signal = undefined, onEvent = async () => {} } = {}) => {
|
|
27
|
+
const headers = { accept: 'text/event-stream' };
|
|
28
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
29
|
+
const response = await fetch(url, { method: 'GET', headers, signal });
|
|
30
|
+
if (!response.ok) return { ok: false, status: response.status };
|
|
31
|
+
let buffer = '';
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
for await (const chunk of response.body) {
|
|
34
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
|
|
35
|
+
let boundary;
|
|
36
|
+
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
|
37
|
+
const block = buffer.slice(0, boundary);
|
|
38
|
+
buffer = buffer.slice(boundary + 2);
|
|
39
|
+
if (!block || block.startsWith(':')) continue;
|
|
40
|
+
let event = 'message';
|
|
41
|
+
const data = [];
|
|
42
|
+
for (const line of block.split('\n')) {
|
|
43
|
+
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
44
|
+
else if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
45
|
+
}
|
|
46
|
+
if (data.length) await onEvent({ event, data: data.join('\n') });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, status: response.status };
|
|
50
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
exports.buildLocalIdentity = (env = process.env) => {
|
|
7
|
+
const device = zero.config.loadDevice(env) || {};
|
|
8
|
+
const host = zero.os.hostname();
|
|
9
|
+
return {
|
|
10
|
+
device_id: device.device_id || null,
|
|
11
|
+
host,
|
|
12
|
+
machine_fingerprint: zero.crypto
|
|
13
|
+
.createHash('sha256')
|
|
14
|
+
.update(`${host}:${process.platform}:${process.arch}`)
|
|
15
|
+
.digest('hex'),
|
|
16
|
+
identity_state: device.device_id ? 'paired' : 'missing'
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
exports.verifyIdentity = (env = process.env) => {
|
|
21
|
+
const identity = exports.buildLocalIdentity(env);
|
|
22
|
+
return {
|
|
23
|
+
ok: identity.identity_state === 'paired',
|
|
24
|
+
identity
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
package/src/index.js
CHANGED
package/src/instance.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
const boot = Date.now();
|
|
5
|
+
const nonce = zero.crypto.randomUUID();
|
|
6
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
const failure = (code) => Object.assign(new Error(code), { code });
|
|
8
|
+
exports.identity = (env = process.env) => {
|
|
9
|
+
const device = zero.config.loadDevice(env);
|
|
10
|
+
if (!device?.device_id || !device.server) throw failure('AGENT_NOT_PAIRED');
|
|
11
|
+
return zero.crypto.createHash('sha256').update(JSON.stringify([
|
|
12
|
+
device.server.replace(/\/+$/, '').toLowerCase(), device.device_id
|
|
13
|
+
])).digest('hex');
|
|
14
|
+
};
|
|
15
|
+
exports.request = (port, message) => new Promise((resolve, reject) => {
|
|
16
|
+
const socket = zero.net.connect({ host: '127.0.0.1', port });
|
|
17
|
+
let data = '';
|
|
18
|
+
socket.setTimeout(1000, () => socket.destroy(failure('AGENT_REPLACE_TIMEOUT')));
|
|
19
|
+
socket.on('error', reject);
|
|
20
|
+
socket.on('connect', () => socket.write(JSON.stringify(message) + '\n'));
|
|
21
|
+
socket.on('data', (chunk) => {
|
|
22
|
+
data += chunk;
|
|
23
|
+
if (data.length > 4096) return socket.destroy(failure('AGENT_INVALID_REPLY'));
|
|
24
|
+
if (!data.includes('\n')) return;
|
|
25
|
+
socket.destroy();
|
|
26
|
+
try { resolve(JSON.parse(data.split('\n')[0])); } catch (error) { reject(error); }
|
|
27
|
+
});
|
|
28
|
+
socket.on('end', () => { if (!data.includes('\n')) reject(failure('AGENT_EMPTY_REPLY')); });
|
|
29
|
+
});
|
|
30
|
+
exports.acquire = async (env = process.env, options = {}) => {
|
|
31
|
+
const identity = options.identity || exports.identity(env);
|
|
32
|
+
const port = options.port || 30000 + parseInt(identity.slice(0, 8), 16) % 20000;
|
|
33
|
+
const order = options.order || [boot, nonce];
|
|
34
|
+
const onReplace = options.onReplace || (() => process.exit(0));
|
|
35
|
+
const deadline = Date.now() + 8000;
|
|
36
|
+
let replacedPid = null;
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
const server = zero.net.createServer((socket) => {
|
|
39
|
+
let data = '';
|
|
40
|
+
socket.setTimeout(1000, () => socket.destroy());
|
|
41
|
+
socket.on('error', () => {});
|
|
42
|
+
socket.on('data', (chunk) => {
|
|
43
|
+
data += chunk;
|
|
44
|
+
if (data.length > 4096) return socket.destroy();
|
|
45
|
+
if (!data.includes('\n')) return;
|
|
46
|
+
socket.removeAllListeners('data');
|
|
47
|
+
let request;
|
|
48
|
+
try { request = JSON.parse(data.split('\n')[0]); } catch { return socket.destroy(); }
|
|
49
|
+
const same = request.protocol === 'zero-agent-instance-v1' && request.identity === identity;
|
|
50
|
+
const newer = Array.isArray(request.order) && (
|
|
51
|
+
request.order[0] > order[0] || (request.order[0] === order[0] && request.order[1] > order[1]));
|
|
52
|
+
const accepted = same && newer && request.action === 'replace';
|
|
53
|
+
socket.end(JSON.stringify({ protocol: 'zero-agent-instance-v1', identity, pid: process.pid,
|
|
54
|
+
accepted, code: same ? 'AGENT_NEWER_INSTANCE_EXISTS' : 'AGENT_PORT_CONFLICT' }) + '\n', () => {
|
|
55
|
+
if (!accepted) return;
|
|
56
|
+
console.log(JSON.stringify({ event: 'agent.instance.replaced', pid: process.pid, by_pid: request.pid }));
|
|
57
|
+
server.close();
|
|
58
|
+
onReplace();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
const error = await new Promise((resolve) => {
|
|
63
|
+
server.once('error', resolve);
|
|
64
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => resolve(null));
|
|
65
|
+
});
|
|
66
|
+
if (!error) {
|
|
67
|
+
server.unref();
|
|
68
|
+
return { pid: process.pid, port, replaced_pid: replacedPid, release: () => server.close() };
|
|
69
|
+
}
|
|
70
|
+
if (error.code !== 'EADDRINUSE') throw error;
|
|
71
|
+
try {
|
|
72
|
+
const reply = await exports.request(port, { protocol: 'zero-agent-instance-v1', identity,
|
|
73
|
+
action: 'replace', pid: process.pid, order });
|
|
74
|
+
if (reply.protocol !== 'zero-agent-instance-v1' || reply.identity !== identity)
|
|
75
|
+
throw failure('AGENT_PORT_CONFLICT');
|
|
76
|
+
if (!reply.accepted) throw failure(reply.code || 'AGENT_REPLACE_REJECTED');
|
|
77
|
+
replacedPid = reply.pid;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (!['ECONNREFUSED', 'ECONNRESET'].includes(error.code)) throw error;
|
|
80
|
+
}
|
|
81
|
+
await sleep(100);
|
|
82
|
+
}
|
|
83
|
+
throw failure('AGENT_PREVIOUS_INSTANCE_DID_NOT_EXIT');
|
|
84
|
+
};
|
|
85
|
+
|