feat(cloud): add password setup command
This commit is contained in:
parent
7831ae5a7b
commit
06ede45212
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"branch": "master",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
"trapos-net": "0.3.2",
|
||||
"trapos-ui": "0.2.4",
|
||||
"trapos-ai": "0.8.0",
|
||||
"trapos-cloud": "0.4.0",
|
||||
"trapos-cloud": "0.4.1",
|
||||
"trapos-sandbox-legacy": "0.3.2",
|
||||
"trapos": "0.12.0"
|
||||
"trapos": "0.12.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-cloud",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "TrapOS cloud gateway client: WS daemon (servers/cloud) + cloud command + MCP exec/write server",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -2,14 +2,20 @@ local createCloud = require('/apis/libcloud');
|
||||
local createVersion = require('/apis/libversion');
|
||||
|
||||
local URL_SETTING = 'cloud.url';
|
||||
local PASSWORD_SETTING = 'cloud.password';
|
||||
|
||||
local rawArgs = table.pack(...);
|
||||
local command = rawArgs[1];
|
||||
|
||||
local function isBlank(value)
|
||||
return type(value) ~= 'string' or value == '';
|
||||
end
|
||||
|
||||
local function printUsage()
|
||||
print('cloud usage:');
|
||||
print();
|
||||
print(' cloud health');
|
||||
print(' cloud status');
|
||||
print(' cloud set-password [--force|-f]');
|
||||
print(' cloud --version');
|
||||
print(' cloud --help');
|
||||
print();
|
||||
@ -19,6 +25,30 @@ local function printUsage()
|
||||
print(' cloud.password (shared secret for the hello handshake)');
|
||||
end
|
||||
|
||||
local function readPassword()
|
||||
write('Password: ');
|
||||
if type(read) == 'function' then return read('*'); end
|
||||
if io and type(io.read) == 'function' then return io.read(); end
|
||||
return '';
|
||||
end
|
||||
|
||||
local function parseSetPasswordArgs(args)
|
||||
local force = false;
|
||||
|
||||
for i = 2, args.n do
|
||||
local arg = args[i];
|
||||
if arg == '--force' or arg == '-f' then
|
||||
force = true;
|
||||
elseif arg and string.sub(arg, 1, 1) == '-' then
|
||||
return nil, 'unknown option: ' .. arg;
|
||||
else
|
||||
return nil, 'unexpected argument: ' .. tostring(arg);
|
||||
end
|
||||
end
|
||||
|
||||
return { force = force }, nil;
|
||||
end
|
||||
|
||||
if command == '--version' or command == '-version' or command == 'version' or command == '-v' then
|
||||
print('v' .. createVersion().forSelf());
|
||||
return;
|
||||
@ -29,9 +59,9 @@ if command == '--help' or command == '-help' or command == 'help' or command ==
|
||||
return;
|
||||
end
|
||||
|
||||
if command == 'health' then
|
||||
if command == 'status' then
|
||||
local url = settings.get(URL_SETTING);
|
||||
if type(url) ~= 'string' or url == '' then
|
||||
if isBlank(url) then
|
||||
print('set cloud.url first');
|
||||
return;
|
||||
end
|
||||
@ -40,7 +70,7 @@ if command == 'health' then
|
||||
if not ok then
|
||||
local code = (type(result) == 'table' and result.code) or 'internal';
|
||||
local message = (type(result) == 'table' and result.message) or '';
|
||||
print('cloud health failed: ' .. tostring(code) .. ' ' .. tostring(message));
|
||||
print('cloud status failed: ' .. tostring(code) .. ' ' .. tostring(message));
|
||||
return;
|
||||
end
|
||||
|
||||
@ -52,4 +82,29 @@ if command == 'health' then
|
||||
return;
|
||||
end
|
||||
|
||||
if command == 'set-password' then
|
||||
local opts, parseError = parseSetPasswordArgs(rawArgs);
|
||||
if parseError then
|
||||
print(parseError);
|
||||
printUsage();
|
||||
return;
|
||||
end
|
||||
|
||||
if not opts.force and not isBlank(settings.get(PASSWORD_SETTING)) then
|
||||
print('cloud.password is already set; use cloud set-password --force to replace it');
|
||||
return;
|
||||
end
|
||||
|
||||
local password = readPassword();
|
||||
if isBlank(password) then
|
||||
print('cloud.password not changed: password cannot be empty');
|
||||
return;
|
||||
end
|
||||
|
||||
settings.set(PASSWORD_SETTING, password);
|
||||
settings.save();
|
||||
print('cloud.password set');
|
||||
return;
|
||||
end
|
||||
|
||||
printUsage();
|
||||
|
||||
192
packages/trapos-cloud/tests/cloud-program.lua
Normal file
192
packages/trapos-cloud/tests/cloud-program.lua
Normal file
@ -0,0 +1,192 @@
|
||||
local createLibTest = require('/apis/libtest');
|
||||
|
||||
local testlib = createLibTest({ ... });
|
||||
|
||||
local function containsLine(lines, expected)
|
||||
for _, line in ipairs(lines) do
|
||||
if line == expected then return true; end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
local function containsText(lines, expected)
|
||||
for _, line in ipairs(lines) do
|
||||
if string.find(line, expected, 1, true) then return true; end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
local function fakeSettings(values)
|
||||
return {
|
||||
values = values or {},
|
||||
saveCount = 0,
|
||||
get = function(self, key)
|
||||
return self.values[key];
|
||||
end,
|
||||
set = function(self, key, value)
|
||||
self.values[key] = value;
|
||||
end,
|
||||
save = function(self)
|
||||
self.saveCount = self.saveCount + 1;
|
||||
return true;
|
||||
end,
|
||||
};
|
||||
end
|
||||
|
||||
local function runCloud(args, opts)
|
||||
opts = opts or {};
|
||||
local lines = {};
|
||||
local writes = {};
|
||||
local readMasks = {};
|
||||
local cloudRequests = {};
|
||||
local settingsLib = opts.settings or fakeSettings();
|
||||
local readValues = opts.readValues or {};
|
||||
|
||||
local env = setmetatable({
|
||||
print = function(line)
|
||||
lines[#lines + 1] = line or '';
|
||||
end,
|
||||
write = function(text)
|
||||
writes[#writes + 1] = text or '';
|
||||
end,
|
||||
read = function(mask)
|
||||
readMasks[#readMasks + 1] = mask;
|
||||
local value = readValues[#readMasks];
|
||||
if value == nil then return ''; end
|
||||
return value;
|
||||
end,
|
||||
settings = {
|
||||
get = function(key) return settingsLib:get(key); end,
|
||||
set = function(key, value) return settingsLib:set(key, value); end,
|
||||
save = function() return settingsLib:save(); end,
|
||||
},
|
||||
require = function(path)
|
||||
if path == '/apis/libcloud' then
|
||||
return function()
|
||||
return {
|
||||
request = function(msgType, payload, requestOpts)
|
||||
cloudRequests[#cloudRequests + 1] = {
|
||||
msgType = msgType,
|
||||
payload = payload,
|
||||
opts = requestOpts,
|
||||
};
|
||||
return opts.requestOk ~= false, opts.requestResult or { serverOk = true, opencodeOk = true };
|
||||
end,
|
||||
};
|
||||
end;
|
||||
end
|
||||
if path == '/apis/libversion' then
|
||||
return function()
|
||||
return { forSelf = function() return '0.0.0-test'; end };
|
||||
end;
|
||||
end
|
||||
return require(path);
|
||||
end,
|
||||
}, { __index = _G });
|
||||
|
||||
local chunk, loadErr = loadfile('/programs/cloud.lua', 't', env);
|
||||
if not chunk then error(loadErr, 0); end
|
||||
local ok, err = pcall(chunk, table.unpack(args or {}));
|
||||
if not ok then error(err, 0); end
|
||||
|
||||
return {
|
||||
lines = lines,
|
||||
writes = writes,
|
||||
readMasks = readMasks,
|
||||
cloudRequests = cloudRequests,
|
||||
settings = settingsLib,
|
||||
};
|
||||
end
|
||||
|
||||
testlib.test('cloud help lists status and set-password without health', function()
|
||||
local ctx = runCloud({ '--help' });
|
||||
|
||||
testlib.assertTrue(containsLine(ctx.lines, ' cloud status'));
|
||||
testlib.assertTrue(containsLine(ctx.lines, ' cloud set-password [--force|-f]'));
|
||||
testlib.assertTrue(not containsLine(ctx.lines, ' cloud health'));
|
||||
end);
|
||||
|
||||
testlib.test('cloud health is not kept as an alias', function()
|
||||
local ctx = runCloud({ 'health' }, {
|
||||
settings = fakeSettings({ ['cloud.url'] = 'wss://example.test' }),
|
||||
});
|
||||
|
||||
testlib.assertEquals(#ctx.cloudRequests, 0);
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'cloud usage:'));
|
||||
end);
|
||||
|
||||
testlib.test('cloud status probes the server', function()
|
||||
local ctx = runCloud({ 'status' }, {
|
||||
settings = fakeSettings({ ['cloud.url'] = 'wss://example.test' }),
|
||||
requestResult = { serverOk = true, opencodeOk = false, opencodeDetail = 'missing key' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(#ctx.cloudRequests, 1);
|
||||
testlib.assertEquals(ctx.cloudRequests[1].msgType, 'probe_server');
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'serverOk: true'));
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'opencodeOk: false'));
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'opencode: missing key'));
|
||||
end);
|
||||
|
||||
testlib.test('cloud set-password stores a missing password with masked input', function()
|
||||
local settingsLib = fakeSettings();
|
||||
local ctx = runCloud({ 'set-password' }, {
|
||||
settings = settingsLib,
|
||||
readValues = { 'hunter2' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(ctx.writes[1], 'Password: ');
|
||||
testlib.assertEquals(ctx.readMasks[1], '*');
|
||||
testlib.assertEquals(settingsLib.values['cloud.password'], 'hunter2');
|
||||
testlib.assertEquals(settingsLib.saveCount, 1);
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'cloud.password set'));
|
||||
end);
|
||||
|
||||
testlib.test('cloud set-password rejects replacing without force', function()
|
||||
local settingsLib = fakeSettings({ ['cloud.password'] = 'old' });
|
||||
local ctx = runCloud({ 'set-password' }, {
|
||||
settings = settingsLib,
|
||||
readValues = { 'new' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(#ctx.readMasks, 0);
|
||||
testlib.assertEquals(settingsLib.values['cloud.password'], 'old');
|
||||
testlib.assertEquals(settingsLib.saveCount, 0);
|
||||
testlib.assertTrue(containsText(ctx.lines, 'use cloud set-password --force'));
|
||||
end);
|
||||
|
||||
testlib.test('cloud set-password --force replaces an existing password', function()
|
||||
local settingsLib = fakeSettings({ ['cloud.password'] = 'old' });
|
||||
runCloud({ 'set-password', '--force' }, {
|
||||
settings = settingsLib,
|
||||
readValues = { 'new' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(settingsLib.values['cloud.password'], 'new');
|
||||
testlib.assertEquals(settingsLib.saveCount, 1);
|
||||
end);
|
||||
|
||||
testlib.test('cloud set-password -f replaces an existing password', function()
|
||||
local settingsLib = fakeSettings({ ['cloud.password'] = 'old' });
|
||||
runCloud({ 'set-password', '-f' }, {
|
||||
settings = settingsLib,
|
||||
readValues = { 'new' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(settingsLib.values['cloud.password'], 'new');
|
||||
testlib.assertEquals(settingsLib.saveCount, 1);
|
||||
end);
|
||||
|
||||
testlib.test('cloud set-password rejects an empty password', function()
|
||||
local settingsLib = fakeSettings();
|
||||
local ctx = runCloud({ 'set-password' }, {
|
||||
settings = settingsLib,
|
||||
readValues = { '' },
|
||||
});
|
||||
|
||||
testlib.assertEquals(settingsLib.values['cloud.password'], nil);
|
||||
testlib.assertEquals(settingsLib.saveCount, 0);
|
||||
testlib.assertTrue(containsLine(ctx.lines, 'cloud.password not changed: password cannot be empty'));
|
||||
end);
|
||||
|
||||
testlib.run();
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": [
|
||||
"trapos-boot",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
-- Full-boot e2e: replicate the real boot path (startup/boot.lua) closely enough
|
||||
-- to prove the whole stack works end to end. Sets the cloud.* settings, creates
|
||||
-- the global event loop, runs the REAL servers/cloud.lua daemon, then under
|
||||
-- parallel waits for the connection and runs the REAL `cloud health` program.
|
||||
-- parallel waits for the connection and runs the REAL `cloud status` program.
|
||||
--
|
||||
-- Args (via shell.run): url, secret ('' => none).
|
||||
-- NOTE: shell.run drops empty-string args; the harness passes '-' for "no secret".
|
||||
@ -23,7 +23,7 @@ parallel.waitForAny(
|
||||
function() _G.bootEventLoop.runLoop(true); end,
|
||||
function()
|
||||
os.pullEvent('trapos_cloud_connected');
|
||||
shell.run('/programs/cloud.lua', 'health');
|
||||
shell.run('/programs/cloud.lua', 'status');
|
||||
end
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user