@miruzero/zero-agent 0.3.4 → 0.3.6
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 +1 -1
- package/package.json +9 -2
- package/src/agent.js +167 -145
- package/src/autostart.js +53 -7
- package/src/capabilities.js +155 -89
- package/src/chatgpt-auth.js +121 -0
- package/src/cli.js +63 -21
- package/src/command/filesystem/append-file.js +33 -0
- package/src/command/filesystem/copy-directory.js +33 -0
- package/src/command/filesystem/copy-file.js +33 -0
- package/src/command/filesystem/create-directory.js +30 -0
- package/src/command/filesystem/create-file.js +35 -0
- package/src/command/filesystem/delete-directory.js +27 -0
- package/src/command/filesystem/delete-file.js +33 -0
- package/src/command/filesystem/exists.js +29 -0
- package/src/command/filesystem/get-file-info.js +39 -0
- package/src/command/filesystem/glob-files.js +39 -0
- package/src/command/filesystem/grep-files.js +54 -0
- package/src/command/filesystem/hash-file.js +35 -0
- package/src/command/filesystem/index.js +34 -0
- package/src/command/filesystem/list-directory.js +33 -0
- package/src/command/filesystem/move-directory.js +30 -0
- package/src/command/filesystem/move-file.js +33 -0
- package/src/command/filesystem/path-policy.js +31 -0
- package/src/command/filesystem/read-file.js +37 -0
- package/src/command/filesystem/read-files.js +56 -0
- package/src/command/filesystem/rename-file.js +20 -0
- package/src/command/filesystem/replace-file.js +45 -0
- package/src/command/filesystem/text.js +17 -0
- package/src/command/filesystem/touch-file.js +29 -0
- package/src/command/filesystem/truncate-file.js +26 -0
- package/src/command/filesystem/walk.js +39 -0
- package/src/command/filesystem/write-file.js +39 -0
- package/src/command/index.js +49 -0
- package/src/command/path/index.js +53 -0
- package/src/index.js +7 -0
- package/src/local-tools.js +205 -171
- package/src/zero-runtime.js +13 -0
package/src/capabilities.js
CHANGED
|
@@ -1,94 +1,160 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const localTools = require('./local-tools');
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const localTools = require('./local-tools');
|
|
7
|
+
const command = require('./command');
|
|
8
|
+
const zero = require('./zero-runtime');
|
|
9
|
+
|
|
10
|
+
const defaultMcpRoot = () => path.resolve(__dirname, '../..');
|
|
11
|
+
const mcpRoot = (env = process.env) => path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
|
|
12
|
+
const providersDir = (env = process.env) => path.join(mcpRoot(env), 'providers');
|
|
13
|
+
|
|
14
|
+
const readManifest = (file) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return { error: error.message };
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const manifestProviders = (env = process.env) => {
|
|
23
|
+
const dir = providersDir(env);
|
|
24
|
+
if (!fs.existsSync(dir)) return [];
|
|
25
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
26
|
+
.filter((item) => item.isDirectory())
|
|
27
|
+
.map((item) => {
|
|
28
|
+
const root = path.join(dir, item.name);
|
|
29
|
+
const manifestFile = path.join(root, 'provider.json');
|
|
30
|
+
const manifest = fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
|
|
31
|
+
return {
|
|
32
|
+
name: item.name,
|
|
33
|
+
type: manifest.type || 'unknown',
|
|
34
|
+
description: manifest.description || '',
|
|
35
|
+
providerRoot: root,
|
|
36
|
+
manifestFile,
|
|
37
|
+
manifestError: manifest.error || null,
|
|
38
|
+
module: manifest.module || null,
|
|
39
|
+
command: manifest.command || null,
|
|
40
|
+
args: manifest.args || []
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const safeInternalTools = (provider) => {
|
|
46
|
+
if (!provider.module) return [];
|
|
47
|
+
const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
|
|
48
|
+
const resolved = path.resolve(modulePath);
|
|
49
|
+
try {
|
|
50
|
+
const runtime = require(resolved);
|
|
51
|
+
if (typeof runtime.listTools !== 'function') return [];
|
|
52
|
+
return runtime.listTools({}).map((tool) => ({
|
|
53
|
+
name: tool.name,
|
|
54
|
+
description: tool.description || '',
|
|
55
|
+
inputSchema: tool.inputSchema || null
|
|
56
|
+
}));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const publicCommandTool = (tool) => ({
|
|
62
|
+
name: tool.name,
|
|
63
|
+
description: tool.description || '',
|
|
64
|
+
canonicalName: tool.name,
|
|
65
|
+
category: tool.category,
|
|
66
|
+
function: tool.function,
|
|
67
|
+
aliases: tool.aliases || [],
|
|
68
|
+
inputSchema: tool.inputSchema || null
|
|
66
69
|
});
|
|
67
70
|
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tool_count: tools.length,
|
|
78
|
-
mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
|
|
79
|
-
};
|
|
80
|
-
})];
|
|
81
|
-
const payload = {
|
|
82
|
-
agent_version: require('../package.json').version,
|
|
83
|
-
mcp_root: mcpRoot(env),
|
|
84
|
-
providers
|
|
71
|
+
const builtInLocalProvider = () => {
|
|
72
|
+
const tools = command.listTools().map(publicCommandTool);
|
|
73
|
+
return {
|
|
74
|
+
name: 'zero-agent-command',
|
|
75
|
+
type: 'internal',
|
|
76
|
+
description: 'Built-in safe command tools exposed by zero-agent.',
|
|
77
|
+
status: 'present',
|
|
78
|
+
tools,
|
|
79
|
+
tool_count: tools.length
|
|
85
80
|
};
|
|
86
|
-
payload.capability_hash = crypto
|
|
87
|
-
.createHash('sha256')
|
|
88
|
-
.update(JSON.stringify(payload.providers))
|
|
89
|
-
.digest('hex');
|
|
90
|
-
return payload;
|
|
91
81
|
};
|
|
92
82
|
|
|
93
|
-
const
|
|
94
|
-
|
|
83
|
+
const builtInChatGptAuthProvider = () => {
|
|
84
|
+
const tools = [
|
|
85
|
+
{
|
|
86
|
+
name: 'status',
|
|
87
|
+
description: 'Return redacted ChatGPT auth fixture status from the local zero-agent host.',
|
|
88
|
+
canonicalName: 'zero.chatgpt.auth.status',
|
|
89
|
+
category: 'auth',
|
|
90
|
+
function: 'status',
|
|
91
|
+
aliases: ['chatgpt.auth.status'],
|
|
92
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'import',
|
|
96
|
+
description: 'Write ChatGPT auth fixture input to the local zero-agent host. Returns redacted metadata only.',
|
|
97
|
+
canonicalName: 'zero.chatgpt.auth.import',
|
|
98
|
+
category: 'auth',
|
|
99
|
+
function: 'import',
|
|
100
|
+
aliases: ['chatgpt.auth.import'],
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: { input: { type: 'string' } },
|
|
104
|
+
required: ['input'],
|
|
105
|
+
additionalProperties: false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
return {
|
|
110
|
+
name: 'chatgpt',
|
|
111
|
+
type: 'internal',
|
|
112
|
+
description: 'Built-in ChatGPT auth setup support exposed by zero-agent.',
|
|
113
|
+
status: 'present',
|
|
114
|
+
tools,
|
|
115
|
+
tool_count: tools.length
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const buildCapabilities = (env = process.env) => {
|
|
120
|
+
const providers = [builtInLocalProvider(), builtInChatGptAuthProvider(), ...manifestProviders(env).map((provider) => {
|
|
121
|
+
const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
|
|
122
|
+
return {
|
|
123
|
+
name: provider.name,
|
|
124
|
+
type: provider.type,
|
|
125
|
+
description: provider.description,
|
|
126
|
+
status: provider.manifestError ? 'invalid' : 'present',
|
|
127
|
+
tools,
|
|
128
|
+
tool_count: tools.length,
|
|
129
|
+
mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
|
|
130
|
+
};
|
|
131
|
+
})];
|
|
132
|
+
const agentVersion = require('../package.json').version;
|
|
133
|
+
const categories = Array.from(new Set(command.listTools().map((tool) => `${tool.provider}.${tool.category}`))).sort();
|
|
134
|
+
const adapters = ['native'];
|
|
135
|
+
const payload = {
|
|
136
|
+
agentVersion,
|
|
137
|
+
agent_version: agentVersion,
|
|
138
|
+
contractVersion: zero.contract.version,
|
|
139
|
+
platform: process.platform,
|
|
140
|
+
architecture: process.arch,
|
|
141
|
+
categories,
|
|
142
|
+
adapters,
|
|
143
|
+
mcp_root: mcpRoot(env),
|
|
144
|
+
providers
|
|
145
|
+
};
|
|
146
|
+
const fingerprint = {
|
|
147
|
+
contractVersion: payload.contractVersion,
|
|
148
|
+
platform: payload.platform,
|
|
149
|
+
architecture: payload.architecture,
|
|
150
|
+
categories: payload.categories,
|
|
151
|
+
adapters: payload.adapters,
|
|
152
|
+
providers: payload.providers
|
|
153
|
+
};
|
|
154
|
+
payload.capability_hash = crypto.createHash('sha256').update(JSON.stringify(fingerprint)).digest('hex');
|
|
155
|
+
return payload;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const capabilities = { mcpRoot, providersDir, manifestProviders, buildCapabilities };
|
|
159
|
+
Object.assign(exports, capabilities);
|
|
160
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
|
|
8
|
+
const provider = 'chatgpt';
|
|
9
|
+
|
|
10
|
+
const defaultMcpHome = (env = process.env) => env.ZERO_MCP_HOME || path.join(os.homedir(), '.zero_mcp');
|
|
11
|
+
const defaultAuthFile = (env = process.env) => path.join(defaultMcpHome(env), 'chatgpt', 'auth.json');
|
|
12
|
+
const resolveAuthFile = (env = process.env) => env.ZERO_CHATGPT_AUTH_FILE || defaultAuthFile(env);
|
|
13
|
+
|
|
14
|
+
const sha256 = (value) => crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
15
|
+
const nowIso = () => new Date().toISOString();
|
|
16
|
+
|
|
17
|
+
const parseMaybeJson = (text) => {
|
|
18
|
+
try {
|
|
19
|
+
return { ok: true, value: JSON.parse(text) };
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return { ok: false, error: error.message || String(error) };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ensureInput = (input) => {
|
|
26
|
+
const text = String(input || '').trim();
|
|
27
|
+
if (!text) {
|
|
28
|
+
const error = new Error('input is required');
|
|
29
|
+
error.code = 'BAD_AUTH_INPUT';
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return text;
|
|
33
|
+
};
|
|
34
|
+
const status = ({ env = process.env } = {}) => {
|
|
35
|
+
const file = resolveAuthFile(env);
|
|
36
|
+
if (!fs.existsSync(file)) {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
provider,
|
|
40
|
+
auth: { present: false, status: 'missing', redacted: true },
|
|
41
|
+
file
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const stat = fs.statSync(file);
|
|
45
|
+
let storedStatus = 'imported-test-fixture';
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
|
+
storedStatus = parsed.status || storedStatus;
|
|
49
|
+
} catch {}
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
provider,
|
|
53
|
+
auth: {
|
|
54
|
+
present: true,
|
|
55
|
+
status: storedStatus,
|
|
56
|
+
redacted: true,
|
|
57
|
+
updated_at: stat.mtime.toISOString(),
|
|
58
|
+
size: stat.size
|
|
59
|
+
},
|
|
60
|
+
file
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const importAuth = ({ input, env = process.env, source = 'zero-agent' } = {}) => {
|
|
65
|
+
const text = ensureInput(input);
|
|
66
|
+
const parsed = parseMaybeJson(text);
|
|
67
|
+
const timestamp = nowIso();
|
|
68
|
+
const payload = {
|
|
69
|
+
provider,
|
|
70
|
+
mode: 'dev-fixture',
|
|
71
|
+
status: 'imported-test-fixture',
|
|
72
|
+
source,
|
|
73
|
+
created_at: timestamp,
|
|
74
|
+
updated_at: timestamp,
|
|
75
|
+
validation: {
|
|
76
|
+
session: 'not-run',
|
|
77
|
+
sentinel: 'not-run',
|
|
78
|
+
capability_probe: 'not-run'
|
|
79
|
+
},
|
|
80
|
+
security: {
|
|
81
|
+
raw_secret_return_allowed: false,
|
|
82
|
+
logs_allowed: false,
|
|
83
|
+
rotation_required: false
|
|
84
|
+
},
|
|
85
|
+
imported: {
|
|
86
|
+
format: parsed.ok ? 'json' : 'text',
|
|
87
|
+
parse_error: parsed.ok ? null : parsed.error,
|
|
88
|
+
sha256: sha256(text),
|
|
89
|
+
byte_length: Buffer.byteLength(text, 'utf8')
|
|
90
|
+
},
|
|
91
|
+
session: parsed.ok ? parsed.value : { raw_text: text }
|
|
92
|
+
};
|
|
93
|
+
const file = resolveAuthFile(env);
|
|
94
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
95
|
+
fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
provider,
|
|
99
|
+
file,
|
|
100
|
+
auth: {
|
|
101
|
+
present: true,
|
|
102
|
+
status: payload.status,
|
|
103
|
+
mode: payload.mode,
|
|
104
|
+
redacted: true,
|
|
105
|
+
updated_at: payload.updated_at,
|
|
106
|
+
input_format: payload.imported.format,
|
|
107
|
+
input_sha256: payload.imported.sha256,
|
|
108
|
+
byte_length: payload.imported.byte_length
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const chatgptAuth = {
|
|
114
|
+
defaultMcpHome,
|
|
115
|
+
defaultAuthFile,
|
|
116
|
+
resolveAuthFile,
|
|
117
|
+
status,
|
|
118
|
+
importAuth
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
Object.assign(exports, chatgptAuth);
|
package/src/cli.js
CHANGED
|
@@ -51,8 +51,10 @@ const ensureLocalBootstrap = async (parsed, env) => {
|
|
|
51
51
|
console.log('autoInit=OK file=' + init.file);
|
|
52
52
|
device = config.loadDevice(env);
|
|
53
53
|
}
|
|
54
|
-
const prepared = autostart.
|
|
55
|
-
|
|
54
|
+
const prepared = autostart.isPrepared(env)
|
|
55
|
+
? { ...autostart.paths(env), ok: true, reused: true }
|
|
56
|
+
: autostart.prepare({ interval: autostartInterval(parsed) }, env);
|
|
57
|
+
console.log('autoSetup=' + (prepared.reused ? 'REUSED' : 'OK') + ' launcher=' + prepared.launcherFile);
|
|
56
58
|
if (parsed.code && (!device || !device.device_id)) {
|
|
57
59
|
const paired = await agent.pairDevice({
|
|
58
60
|
name: parsed.name || device?.name,
|
|
@@ -87,7 +89,11 @@ const runSetup = async (parsed, env) => {
|
|
|
87
89
|
console.log('Zero Agent Setup');
|
|
88
90
|
console.log('server=' + server);
|
|
89
91
|
console.log('name=' + (name || os.hostname()));
|
|
90
|
-
runGlobalInstall(parsed, env);
|
|
92
|
+
const installed = runGlobalInstall(parsed, env);
|
|
93
|
+
if (!installed.skipped && !installed.ok) {
|
|
94
|
+
console.error('setup=FAILED global installation did not complete');
|
|
95
|
+
return 70;
|
|
96
|
+
}
|
|
91
97
|
const init = agent.initDevice({ name, server }, env);
|
|
92
98
|
console.log('init=OK file=' + init.file);
|
|
93
99
|
const prepared = autostart.prepare({ interval: autostartInterval(parsed) }, env);
|
|
@@ -136,6 +142,21 @@ const runStartOnce = async (env) => {
|
|
|
136
142
|
return { heartbeat, capability, task };
|
|
137
143
|
};
|
|
138
144
|
|
|
145
|
+
const runSafeStartCycle = async (env, runOnce = runStartOnce) => {
|
|
146
|
+
try {
|
|
147
|
+
return { ok: true, result: await runOnce(env) };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reconnect: true,
|
|
152
|
+
error: {
|
|
153
|
+
code: error.code || error.cause?.code || 'AGENT_CONNECTION_ERROR',
|
|
154
|
+
message: error.message || String(error)
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
139
160
|
const printStartBanner = (env) => {
|
|
140
161
|
const device = config.loadDevice(env);
|
|
141
162
|
console.log('🚀 Starting Zero Agent...');
|
|
@@ -145,26 +166,38 @@ const printStartBanner = (env) => {
|
|
|
145
166
|
console.log('🌐 Server: ' + (device?.server || 'https://zero.miru.work'));
|
|
146
167
|
};
|
|
147
168
|
|
|
148
|
-
const
|
|
169
|
+
const loopResultEvent = (result) => {
|
|
149
170
|
if (!result || result.code === 'NOT_PAIRED') {
|
|
150
|
-
|
|
151
|
-
return;
|
|
171
|
+
return { level: 'error', text: '❌ Device not paired: run zero-agent pair first' };
|
|
152
172
|
}
|
|
153
173
|
const hb = result.heartbeat || {};
|
|
154
174
|
const cap = result.capability || {};
|
|
175
|
+
const task = result.task?.task || null;
|
|
176
|
+
const healthy = Boolean(hb.ok) && Boolean(cap.ok);
|
|
177
|
+
if (healthy && !task) return null;
|
|
178
|
+
|
|
155
179
|
const device = cap.body?.device || hb.body?.device || null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
return {
|
|
181
|
+
level: healthy ? 'info' : 'error',
|
|
182
|
+
text: JSON.stringify({
|
|
183
|
+
at: new Date().toISOString(),
|
|
184
|
+
heartbeat: okMark(Boolean(hb.ok)),
|
|
185
|
+
capabilities: okMark(Boolean(cap.ok)),
|
|
186
|
+
status: device?.status || null,
|
|
187
|
+
name: device?.name || null,
|
|
188
|
+
host: device?.host || null,
|
|
189
|
+
providers: device?.provider_count ?? null,
|
|
190
|
+
last_seen: device?.last_seen || null,
|
|
191
|
+
task: task ? (task.task_id + ':' + (task.status || 'done')) : 'none'
|
|
192
|
+
})
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const printLoopResult = (result) => {
|
|
197
|
+
const event = loopResultEvent(result);
|
|
198
|
+
if (!event) return;
|
|
199
|
+
if (event.level === 'error') console.error(event.text);
|
|
200
|
+
else console.log(event.text);
|
|
168
201
|
};
|
|
169
202
|
|
|
170
203
|
async function run(argv = [], env = process.env) {
|
|
@@ -225,8 +258,17 @@ async function run(argv = [], env = process.env) {
|
|
|
225
258
|
console.log('⏱️ Heartbeat interval: ' + interval + 'ms');
|
|
226
259
|
console.log('🟢 Device ready; press Ctrl+C to stop');
|
|
227
260
|
while (true) {
|
|
228
|
-
const
|
|
229
|
-
|
|
261
|
+
const cycle = await runSafeStartCycle(env);
|
|
262
|
+
if (cycle.ok) {
|
|
263
|
+
printLoopResult(cycle.result);
|
|
264
|
+
} else {
|
|
265
|
+
console.error(JSON.stringify({
|
|
266
|
+
at: new Date().toISOString(),
|
|
267
|
+
heartbeat: 'FAIL',
|
|
268
|
+
reconnect: 'WAIT',
|
|
269
|
+
error: cycle.error
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
230
272
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
231
273
|
}
|
|
232
274
|
}
|
|
@@ -235,4 +277,4 @@ async function run(argv = [], env = process.env) {
|
|
|
235
277
|
return 64;
|
|
236
278
|
}
|
|
237
279
|
|
|
238
|
-
module.exports = { run, usage, status, doctor };
|
|
280
|
+
module.exports = { run, usage, status, doctor, runSafeStartCycle, loopResultEvent };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const zero = require('../../zero-runtime');
|
|
6
|
+
const pathPolicy = require('./path-policy');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.appendFile',
|
|
10
|
+
aliases: ['zero.command.appendFile'],
|
|
11
|
+
description: 'Append UTF-8 text content to a file inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.write',
|
|
13
|
+
risk: 'write',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } },
|
|
19
|
+
required: ['path', 'content'],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const appendFile = (args = {}, context = {}) => {
|
|
25
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
26
|
+
const data = String(args.content ?? '');
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
fs.appendFileSync(file, data, 'utf8');
|
|
29
|
+
return { ok: true, path: file, mode: 'append', bytes: Buffer.byteLength(data) };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const appendFileModule = { descriptor, appendFile };
|
|
33
|
+
Object.assign(exports, appendFileModule);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const zero = require('../../zero-runtime');
|
|
5
|
+
const pathPolicy = require('./path-policy');
|
|
6
|
+
|
|
7
|
+
const descriptor = zero.descriptor.validate({
|
|
8
|
+
name: 'zero.command.filesystem.copyDirectory',
|
|
9
|
+
aliases: ['zero.command.copyDirectory'],
|
|
10
|
+
description: 'Copy a directory recursively inside allowed roots.',
|
|
11
|
+
capability: 'filesystem.write',
|
|
12
|
+
risk: 'write',
|
|
13
|
+
execution: 'target',
|
|
14
|
+
adapters: ['native'],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: { source: { type: 'string' }, destination: { type: 'string' }, overwrite: { type: 'boolean', default: false } },
|
|
18
|
+
required: ['source', 'destination'],
|
|
19
|
+
additionalProperties: false
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
24
|
+
const copyDirectory = (args = {}, context = {}) => {
|
|
25
|
+
const source = pathPolicy.resolveAllowed(args.source, context);
|
|
26
|
+
const destination = pathPolicy.resolveAllowed(args.destination, context);
|
|
27
|
+
if (!fs.statSync(source).isDirectory()) throw fail('NOT_A_DIRECTORY', 'source is not a directory');
|
|
28
|
+
fs.cpSync(source, destination, { recursive: true, force: args.overwrite === true, errorOnExist: args.overwrite !== true });
|
|
29
|
+
return { ok: true, source, destination };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const copyDirectoryModule = { descriptor, copyDirectory };
|
|
33
|
+
Object.assign(exports, copyDirectoryModule);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const zero = require('../../zero-runtime');
|
|
6
|
+
const pathPolicy = require('./path-policy');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.copyFile',
|
|
10
|
+
aliases: ['zero.command.copyFile'],
|
|
11
|
+
description: 'Copy one file inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.write',
|
|
13
|
+
risk: 'write',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { source: { type: 'string' }, destination: { type: 'string' } },
|
|
19
|
+
required: ['source', 'destination'],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const copyFile = (args = {}, context = {}) => {
|
|
25
|
+
const source = pathPolicy.resolveAllowed(args.source, context);
|
|
26
|
+
const destination = pathPolicy.resolveAllowed(args.destination, context);
|
|
27
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
28
|
+
fs.copyFileSync(source, destination);
|
|
29
|
+
return { ok: true, source, destination };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const copyFileModule = { descriptor, copyFile };
|
|
33
|
+
Object.assign(exports, copyFileModule);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const zero = require('../../zero-runtime');
|
|
5
|
+
const pathPolicy = require('./path-policy');
|
|
6
|
+
|
|
7
|
+
const descriptor = zero.descriptor.validate({
|
|
8
|
+
name: 'zero.command.filesystem.createDirectory',
|
|
9
|
+
aliases: ['zero.command.createDirectory'],
|
|
10
|
+
description: 'Create a directory recursively inside allowed roots.',
|
|
11
|
+
capability: 'filesystem.write',
|
|
12
|
+
risk: 'write',
|
|
13
|
+
execution: 'target',
|
|
14
|
+
adapters: ['native'],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: { path: { type: 'string' } },
|
|
18
|
+
required: ['path'],
|
|
19
|
+
additionalProperties: false
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createDirectory = (args = {}, context = {}) => {
|
|
24
|
+
const dir = pathPolicy.resolveAllowed(args.path, context);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
return { ok: true, path: dir, created: fs.existsSync(dir) && fs.statSync(dir).isDirectory() };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createDirectoryModule = { descriptor, createDirectory };
|
|
30
|
+
Object.assign(exports, createDirectoryModule);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const zero = require('../../zero-runtime');
|
|
6
|
+
const pathPolicy = require('./path-policy');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.createFile',
|
|
10
|
+
aliases: ['zero.command.createFile'],
|
|
11
|
+
description: 'Create a UTF-8 text file inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.write',
|
|
13
|
+
risk: 'write',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { path: { type: 'string' }, content: { type: 'string', default: '' }, overwrite: { type: 'boolean', default: false } },
|
|
19
|
+
required: ['path'],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
25
|
+
const createFile = (args = {}, context = {}) => {
|
|
26
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
27
|
+
if (fs.existsSync(file) && args.overwrite !== true) throw fail('FILE_EXISTS', 'file already exists');
|
|
28
|
+
const data = String(args.content ?? '');
|
|
29
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
30
|
+
fs.writeFileSync(file, data, 'utf8');
|
|
31
|
+
return { ok: true, path: file, created: true, bytes: Buffer.byteLength(data) };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createFileModule = { descriptor, createFile };
|
|
35
|
+
Object.assign(exports, createFileModule);
|