feat(cloud): add password setup command

This commit is contained in:
Guillaume ARM 2026-06-17 02:41:48 +02:00
parent 7831ae5a7b
commit 06ede45212
7 changed files with 258 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{
"name": "TrapOS",
"version": "0.12.0",
"version": "0.12.1",
"branch": "master",
"packages": [
"trapos"

View File

@ -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"
}
}

View File

@ -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": [

View File

@ -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();

View 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();

View File

@ -1,6 +1,6 @@
{
"name": "trapos",
"version": "0.12.0",
"version": "0.12.1",
"description": "TrapOS full install meta-package",
"dependencies": [
"trapos-boot",

View File

@ -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
);