@lifeaitools/clauth 1.30.26 → 2.0.0
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/.clauth-skill/references/keys-guide.md +270 -270
- package/README.md +2 -0
- package/cli/api.js +238 -238
- package/cli/commands/agent-pool.js +51 -15
- package/cli/commands/install.js +396 -396
- package/cli/commands/login.js +135 -0
- package/cli/commands/login.test.js +73 -0
- package/cli/commands/serve.js +290 -1555
- package/cli/commands/uninstall.js +164 -164
- package/cli/index.js +102 -1
- package/cli/supervisor-registry.js +106 -5
- package/cli/supervisor-registry.test.js +151 -0
- package/cli/webdav-service.js +339 -339
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -5
- package/scripts/bootstrap.cjs +121 -121
- package/scripts/build.mjs +66 -0
- package/scripts/build.sh +5 -45
- package/scripts/postinstall.js +189 -189
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
listPlugins,
|
|
12
12
|
listSurfaces,
|
|
13
13
|
reconcileSurfaceHealth,
|
|
14
|
+
registerPlugin,
|
|
14
15
|
runPluginAction,
|
|
15
16
|
runSurfaceAction,
|
|
16
17
|
removeTunnelRoute,
|
|
17
18
|
setPluginEnabled,
|
|
19
|
+
shellQuote,
|
|
18
20
|
supervisorHealth,
|
|
19
21
|
validatePluginManifest,
|
|
20
22
|
} from "./supervisor-registry.js";
|
|
@@ -50,6 +52,15 @@ function writePlugin(root, source, id, manifest) {
|
|
|
50
52
|
return dir;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// A scoped npm package installs at <root>/@scope/pkg-name/clauth-plugin.json —
|
|
56
|
+
// two levels deep, mirroring node_modules/@lifeaitools/<pkg>/.
|
|
57
|
+
function writeScopedPlugin(root, source, scope, id, manifest) {
|
|
58
|
+
const dir = path.join(root, source, scope, id);
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
function baseManifest(id, overrides = {}) {
|
|
54
65
|
return {
|
|
55
66
|
schema: "lifeai.plugin.v1",
|
|
@@ -94,6 +105,32 @@ test("validatePluginManifest accepts empty test command arrays from the v1 templ
|
|
|
94
105
|
assert.equal(plugin.test.port, "auto");
|
|
95
106
|
});
|
|
96
107
|
|
|
108
|
+
test("shellQuote quotes a whitespace-bearing value only when shell is active, and is idempotent", () => {
|
|
109
|
+
assert.equal(shellQuote("pm2", true), "pm2");
|
|
110
|
+
assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", true), '"C:/Program Files/nodejs/node.exe"');
|
|
111
|
+
assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", false), "C:/Program Files/nodejs/node.exe");
|
|
112
|
+
assert.equal(shellQuote("one two", false), "one two");
|
|
113
|
+
assert.equal(shellQuote('"already quoted value"', true), '"already quoted value"');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("validatePluginManifest rejects shell metacharacters in command ARGS, not just command[0]", () => {
|
|
117
|
+
// The [;&|<>] guard on command[0] alone is defeated by moving the payload
|
|
118
|
+
// one index right — args must be validated the same way (verified live
|
|
119
|
+
// during code review: spawnSync(['cmd','/c','echo','A & echo INJECTED'],
|
|
120
|
+
// {shell:true}) executed a second command from inside one arg string).
|
|
121
|
+
assert.throws(() => validatePluginManifest(baseManifest("inject", {
|
|
122
|
+
surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "start svc & calc.exe"] }],
|
|
123
|
+
})), /shell syntax/);
|
|
124
|
+
assert.throws(() => validatePluginManifest(baseManifest("inject2", {
|
|
125
|
+
surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "name; rm -rf /"] }],
|
|
126
|
+
})), /shell syntax/);
|
|
127
|
+
// A legitimate arg with no shell metacharacters still passes.
|
|
128
|
+
const ok = validatePluginManifest(baseManifest("legit", {
|
|
129
|
+
surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "my-app-name"] }],
|
|
130
|
+
}), "clauth-plugin.json");
|
|
131
|
+
assert.deepEqual(ok.surfaces[0].restart, ["pm2", "restart", "my-app-name"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
97
134
|
test("validatePluginManifest rejects invalid manifests and non-local health URLs", () => {
|
|
98
135
|
assert.throws(() => validatePluginManifest({ ...baseManifest("bad"), schema: "bad" }), /schema/);
|
|
99
136
|
assert.throws(() => validatePluginManifest(baseManifest("bad", {
|
|
@@ -126,6 +163,50 @@ test("discovery merges managed and user roots without silent managed-id shadowin
|
|
|
126
163
|
assert.match(invalid.error, /shadow/);
|
|
127
164
|
}));
|
|
128
165
|
|
|
166
|
+
test("discovery finds both a scoped npm-package layout and an unscoped layout from one root", () => withTempSupervisor((root) => {
|
|
167
|
+
const managed = path.join(root, "managed");
|
|
168
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
169
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
170
|
+
// Unscoped: managed/plain-plugin/clauth-plugin.json (one level — existing behavior)
|
|
171
|
+
writePlugin(root, "managed", "plain-plugin", baseManifest("plain-plugin"));
|
|
172
|
+
// Scoped: managed/@lifeaitools/fs-mcp/clauth-plugin.json (two levels — the WP-1 fix)
|
|
173
|
+
writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp"));
|
|
174
|
+
|
|
175
|
+
const result = discoverPlugins();
|
|
176
|
+
const plain = result.plugins.find((plugin) => plugin.id === "plain-plugin");
|
|
177
|
+
const scoped = result.plugins.find((plugin) => plugin.id === "fs-mcp");
|
|
178
|
+
assert.ok(plain, "unscoped layout must still be discovered");
|
|
179
|
+
assert.equal(plain.state, "awaiting_enable");
|
|
180
|
+
assert.ok(scoped, "scoped @scope/pkg layout must now be discovered");
|
|
181
|
+
assert.equal(scoped.state, "awaiting_enable");
|
|
182
|
+
assert.equal(scoped.manifest_hash?.length > 0, true);
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
test("discovery skips a malformed manifest without aborting the rest of the scan", () => withTempSupervisor((root) => {
|
|
186
|
+
const managed = path.join(root, "managed");
|
|
187
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
188
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
189
|
+
writePlugin(root, "managed", "good-plugin", baseManifest("good-plugin"));
|
|
190
|
+
const brokenDir = path.join(managed, "broken-plugin");
|
|
191
|
+
fs.mkdirSync(brokenDir, { recursive: true });
|
|
192
|
+
fs.writeFileSync(path.join(brokenDir, "clauth-plugin.json"), "{ this is not valid json", "utf8");
|
|
193
|
+
// Also prove a malformed SCOPED manifest doesn't abort the scoped-dir descent either.
|
|
194
|
+
const brokenScopedDir = path.join(managed, "@lifeaitools", "broken-scoped");
|
|
195
|
+
fs.mkdirSync(brokenScopedDir, { recursive: true });
|
|
196
|
+
fs.writeFileSync(path.join(brokenScopedDir, "clauth-plugin.json"), "{ also not valid json", "utf8");
|
|
197
|
+
writeScopedPlugin(root, "managed", "@lifeaitools", "good-scoped", baseManifest("good-scoped"));
|
|
198
|
+
|
|
199
|
+
const result = discoverPlugins();
|
|
200
|
+
const good = result.plugins.find((plugin) => plugin.id === "good-plugin");
|
|
201
|
+
const broken = result.plugins.find((plugin) => plugin.id === "broken-plugin");
|
|
202
|
+
const goodScoped = result.plugins.find((plugin) => plugin.id === "good-scoped");
|
|
203
|
+
const brokenScoped = result.plugins.find((plugin) => plugin.id === "broken-scoped");
|
|
204
|
+
assert.equal(good.state, "awaiting_enable");
|
|
205
|
+
assert.equal(broken.state, "manifest_invalid");
|
|
206
|
+
assert.equal(goodScoped.state, "awaiting_enable");
|
|
207
|
+
assert.equal(brokenScoped.state, "manifest_invalid");
|
|
208
|
+
}));
|
|
209
|
+
|
|
129
210
|
test("trusted managed core plugins auto-enable while user plugins remain opt-in", () => withTempSupervisor((root) => {
|
|
130
211
|
const managed = path.join(root, "managed");
|
|
131
212
|
const user = path.join(root, "user");
|
|
@@ -378,6 +459,76 @@ test("supervisor log DTO strips raw operation payloads", () => {
|
|
|
378
459
|
assert.equal(json.includes("--token"), false);
|
|
379
460
|
});
|
|
380
461
|
|
|
462
|
+
test("registerPlugin validates, writes into the managed root, and discovers the plugin — idempotent on re-register", () => withTempSupervisor((root) => {
|
|
463
|
+
const managed = path.join(root, "managed");
|
|
464
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
465
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
466
|
+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-source-"));
|
|
467
|
+
const manifestPath = path.join(sourceDir, "clauth-plugin.json");
|
|
468
|
+
fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("registered-demo", { core: true, enable_default: true })), "utf8");
|
|
469
|
+
|
|
470
|
+
const first = registerPlugin(manifestPath, "test");
|
|
471
|
+
assert.equal(first.resulting_state.ok, true);
|
|
472
|
+
assert.equal(first.resulting_state.state, "registered");
|
|
473
|
+
assert.equal(first.resulting_state.plugin_state, "current");
|
|
474
|
+
const written = fs.existsSync(path.join(managed, "registered-demo", "clauth-plugin.json"));
|
|
475
|
+
assert.equal(written, true);
|
|
476
|
+
const found = listPlugins().find((plugin) => plugin.id === "registered-demo");
|
|
477
|
+
assert.equal(found.enabled, true);
|
|
478
|
+
|
|
479
|
+
const second = registerPlugin(manifestPath, "test");
|
|
480
|
+
assert.equal(second.resulting_state.state, "unchanged", "re-registering identical content must be a no-op, not a rewrite");
|
|
481
|
+
|
|
482
|
+
fs.rmSync(sourceDir, { recursive: true, force: true });
|
|
483
|
+
}));
|
|
484
|
+
|
|
485
|
+
test("registerPlugin rejects a plugin id that would escape the managed-plugins root", () => withTempSupervisor((root) => {
|
|
486
|
+
// Code-review finding (confidence 95, live PoC): manifest.id of ".." passed
|
|
487
|
+
// the old id regex (dot is in the allowed character class with no
|
|
488
|
+
// exclusion of all-dots forms) and path.join(managedRoot, "..") wrote
|
|
489
|
+
// clauth-plugin.json one level ABOVE the managed root. Two independent
|
|
490
|
+
// fixes now close this: the id regex rejects all-dots ids, and
|
|
491
|
+
// registerPlugin asserts containment at the write site so a future regex
|
|
492
|
+
// relaxation can't reopen it.
|
|
493
|
+
const managed = path.join(root, "managed");
|
|
494
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
495
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
496
|
+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-traversal-"));
|
|
497
|
+
const manifestPath = path.join(sourceDir, "clauth-plugin.json");
|
|
498
|
+
fs.writeFileSync(manifestPath, JSON.stringify({ schema: "lifeai.plugin.v1", id: "..", version: "1.0.0", publisher: "t", documentation: { architecture: "a.md" }, surfaces: [] }), "utf8");
|
|
499
|
+
|
|
500
|
+
const receipt = registerPlugin(manifestPath, "test");
|
|
501
|
+
assert.equal(receipt.resulting_state.ok, false);
|
|
502
|
+
assert.equal(receipt.resulting_state.state, "manifest_invalid");
|
|
503
|
+
const escapedPath = path.join(managed, "..", "clauth-plugin.json");
|
|
504
|
+
assert.equal(fs.existsSync(escapedPath), false, "must never write outside the managed-plugins root");
|
|
505
|
+
|
|
506
|
+
fs.rmSync(sourceDir, { recursive: true, force: true });
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
test("validatePluginManifest rejects an all-dots plugin or surface id", () => {
|
|
510
|
+
assert.throws(() => validatePluginManifest(baseManifest("..", {}), "clauth-plugin.json"), /may not be all dots/);
|
|
511
|
+
assert.throws(() => validatePluginManifest(baseManifest("valid-id", {
|
|
512
|
+
surfaces: [{ id: ".", lifecycle_owner: "clauth" }],
|
|
513
|
+
}), "clauth-plugin.json"), /may not be all dots/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("registerPlugin rejects an invalid manifest without writing anything", () => withTempSupervisor((root) => {
|
|
517
|
+
const managed = path.join(root, "managed");
|
|
518
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
519
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
520
|
+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-bad-"));
|
|
521
|
+
const manifestPath = path.join(sourceDir, "clauth-plugin.json");
|
|
522
|
+
fs.writeFileSync(manifestPath, JSON.stringify({ schema: "wrong", id: "bad" }), "utf8");
|
|
523
|
+
|
|
524
|
+
const receipt = registerPlugin(manifestPath, "test");
|
|
525
|
+
assert.equal(receipt.resulting_state.ok, false);
|
|
526
|
+
assert.equal(receipt.resulting_state.state, "manifest_invalid");
|
|
527
|
+
assert.equal(fs.existsSync(path.join(managed, "bad")), false);
|
|
528
|
+
|
|
529
|
+
fs.rmSync(sourceDir, { recursive: true, force: true });
|
|
530
|
+
}));
|
|
531
|
+
|
|
381
532
|
test("tunnel route add and remove produce reversible operation receipts", () => withTempSupervisor((root) => {
|
|
382
533
|
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
|
|
383
534
|
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|