This commit is contained in:
@@ -11,7 +11,7 @@ describe('user-folder/paths', () => {
|
||||
|
||||
it('creates the standard subdirs on first ensure', () => {
|
||||
ensureUserFolder(root, 'user-abc');
|
||||
for (const sub of ['scripts', 'browser-macros', 'templates', 'recordings', 'trash', 'memory', 'pets']) {
|
||||
for (const sub of ['browser-macros', 'recordings', 'trash', 'memory', 'pets', 'notes']) {
|
||||
expect(existsSync(join(root, 'user-abc', sub))).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -35,17 +35,17 @@ describe('user-folder/paths', () => {
|
||||
});
|
||||
|
||||
it('resolves subdir paths under the owner root', () => {
|
||||
const p = resolveUserSubdir(root, 'user-abc', 'scripts', 'foo.js');
|
||||
expect(p).toBe(join(root, 'user-abc', 'scripts', 'foo.js'));
|
||||
const p = resolveUserSubdir(root, 'user-abc', 'browser-macros', 'foo.js');
|
||||
expect(p).toBe(join(root, 'user-abc', 'browser-macros', 'foo.js'));
|
||||
});
|
||||
|
||||
it('rejects path traversal in the relative segment', () => {
|
||||
expect(() => resolveUserSubdir(root, 'user-abc', 'scripts', '../../etc/passwd'))
|
||||
expect(() => resolveUserSubdir(root, 'user-abc', 'browser-macros', '../../etc/passwd'))
|
||||
.toThrow(/outside owner folder/);
|
||||
});
|
||||
|
||||
it('rejects empty relPath', () => {
|
||||
expect(() => resolveUserSubdir(root, 'user-abc', 'scripts', ''))
|
||||
expect(() => resolveUserSubdir(root, 'user-abc', 'browser-macros', ''))
|
||||
.toThrow(/relPath must not be empty/);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ export const LOCAL_SYSTEM_OWNER_ID = 'local';
|
||||
|
||||
const USER_AGENTS_MAX_BYTES = 64 * 1024;
|
||||
|
||||
export const USER_SUBDIRS = ['scripts', 'browser-macros', 'templates', 'recordings', 'trash', 'memory', 'pets', 'notes'] as const;
|
||||
// 'scripts' and 'templates' were retired in 2026-06 (superseded by Skills +
|
||||
// the Bash tool). Existing files stay on disk but are no longer created,
|
||||
// listed, or resolvable through the user-folder API / tools.
|
||||
export const USER_SUBDIRS = ['browser-macros', 'recordings', 'trash', 'memory', 'pets', 'notes'] as const;
|
||||
export type UserSubdir = typeof USER_SUBDIRS[number];
|
||||
|
||||
export function userRoot(rootDir: string, ownerId: string): string {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
/**
|
||||
* script-orchestrator.ts
|
||||
*
|
||||
* Shared "resolve a user script by name + run it" helper. Used by both:
|
||||
* Shared "resolve a browser-macro by name + run it" helper. Used by both:
|
||||
* - The LLM-facing RunUserScript tool (engine/tools/user-folder.ts).
|
||||
* - The scheduler's script kind (scheduler.ts), so a periodic script run
|
||||
* - The scheduler's script kind (scheduler.ts), so a periodic macro run
|
||||
* does not need to spin up an LLM agent loop.
|
||||
*
|
||||
* Note: plain-Node `scripts/` and `templates/` were retired in 2026-06
|
||||
* (superseded by Skills + the Bash tool). The only user-script runtime left
|
||||
* is Playwright browser-macros.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Path resolution under data/users/{userId}/{scripts,browser-macros}/ with
|
||||
* - Path resolution under data/users/{userId}/browser-macros/ with
|
||||
* traversal protection (delegated to resolveUserSubdir).
|
||||
* - Frontmatter parsing for browser-macros to find session_profile_id.
|
||||
* - Frontmatter parsing to find session_profile_id.
|
||||
* - Decrypting + loading Playwright storageState for the owning user.
|
||||
* - Calling runUserScript() with the right runtime.
|
||||
* - Calling runUserScript() with the playwright runtime.
|
||||
*
|
||||
* Intentionally NOT responsible for:
|
||||
* - Config gating (tools.user_scripts_enabled) — callers decide.
|
||||
@@ -27,9 +31,9 @@ import { runUserScript } from './script-runner.js';
|
||||
import { loadSessionStateForUser } from './session-loader.js';
|
||||
import type { BrowserSessionRepo } from '../db/browser-session-repo.js';
|
||||
|
||||
export type ScriptKind = 'script' | 'browser-macro';
|
||||
export type ScriptSubdir = 'scripts' | 'browser-macros';
|
||||
export type ScriptRuntime = 'plain' | 'playwright';
|
||||
export type ScriptKind = 'browser-macro';
|
||||
export type ScriptSubdir = 'browser-macros';
|
||||
export type ScriptRuntime = 'playwright';
|
||||
|
||||
export interface ResolveScriptResult {
|
||||
scriptPath: string;
|
||||
@@ -37,49 +41,31 @@ export interface ResolveScriptResult {
|
||||
runtime: ScriptRuntime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a script name to its on-disk path. When kind is omitted, scripts/
|
||||
* is searched first, then browser-macros/.
|
||||
*/
|
||||
/** Resolve a macro name to its on-disk path under browser-macros/. */
|
||||
export function resolveScriptForKind(
|
||||
rootDir: string,
|
||||
userId: string,
|
||||
scriptName: string,
|
||||
kind: ScriptKind | undefined,
|
||||
_kind?: ScriptKind,
|
||||
): ResolveScriptResult | { error: string } {
|
||||
const tryOne = (sd: ScriptSubdir): string | null => {
|
||||
try {
|
||||
const p = resolveUserSubdir(rootDir, userId, sd, scriptName);
|
||||
return existsSync(p) ? p : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (kind === 'script') {
|
||||
const p = tryOne('scripts');
|
||||
if (!p) return { error: `script not found: scripts/${basename(scriptName)}` };
|
||||
return { scriptPath: p, subdir: 'scripts', runtime: 'plain' };
|
||||
let p: string | null;
|
||||
try {
|
||||
const full = resolveUserSubdir(rootDir, userId, 'browser-macros', scriptName);
|
||||
p = existsSync(full) ? full : null;
|
||||
} catch {
|
||||
p = null;
|
||||
}
|
||||
if (kind === 'browser-macro') {
|
||||
const p = tryOne('browser-macros');
|
||||
if (!p) return { error: `browser-macro not found: browser-macros/${basename(scriptName)}` };
|
||||
return { scriptPath: p, subdir: 'browser-macros', runtime: 'playwright' };
|
||||
}
|
||||
const sp = tryOne('scripts');
|
||||
if (sp) return { scriptPath: sp, subdir: 'scripts', runtime: 'plain' };
|
||||
const bp = tryOne('browser-macros');
|
||||
if (bp) return { scriptPath: bp, subdir: 'browser-macros', runtime: 'playwright' };
|
||||
return { error: `script not found: ${scriptName} (searched scripts/ and browser-macros/)` };
|
||||
if (!p) return { error: `browser-macro not found: browser-macros/${basename(scriptName)}` };
|
||||
return { scriptPath: p, subdir: 'browser-macros', runtime: 'playwright' };
|
||||
}
|
||||
|
||||
export interface ResolveAndRunOptions {
|
||||
rootDir: string;
|
||||
userId: string;
|
||||
/** Script name with or without `.js` extension. */
|
||||
/** Macro name with or without `.js` extension. */
|
||||
name: string;
|
||||
params: Record<string, unknown>;
|
||||
/** If omitted, scripts/ is tried first, then browser-macros/. */
|
||||
/** Kept for call-site compatibility; the only kind is 'browser-macro'. */
|
||||
kind?: ScriptKind;
|
||||
/** Required only when the resolved script is a browser-macro that declares session_profile_id. */
|
||||
sessRepo?: BrowserSessionRepo;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* template-renderer.ts
|
||||
*
|
||||
* Simple {{var}} substitution for user templates. Intentionally minimal:
|
||||
* - No conditionals, no loops, no helpers. If those become needed,
|
||||
* graduate to Handlebars in a follow-up.
|
||||
* - Unknown placeholders (var not declared in frontmatter.params) are
|
||||
* left literal — so README-style templates with prose like "use {{x}}"
|
||||
* don't blow up when there's no x param.
|
||||
*
|
||||
* Param semantics (type-check + defaults) are shared with scripts via
|
||||
* validateAndApplyDefaults from script-runner.ts; templates use the same
|
||||
* frontmatter.params schema as scripts/browser-macros.
|
||||
*/
|
||||
|
||||
import type { ParamSpec } from './frontmatter.js';
|
||||
import { validateAndApplyDefaults } from './script-runner.js';
|
||||
|
||||
/**
|
||||
* Replaces {{name}} with the corresponding param value, but only for params
|
||||
* that appear in `declared` (the validated set). Unknown {{xxx}} stays literal.
|
||||
*/
|
||||
export function renderTemplate(
|
||||
body: string,
|
||||
paramSpec: ParamSpec[],
|
||||
rawParams: Record<string, unknown>,
|
||||
): string {
|
||||
const resolved = validateAndApplyDefaults(paramSpec, rawParams);
|
||||
return body.replace(/\{\{(\w+)\}\}/g, (match, name) =>
|
||||
Object.prototype.hasOwnProperty.call(resolved, name) ? String(resolved[name]) : match,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user