unify agent items into a flat file-based store, drop the marketplace

Replace the marketplace service dependency and the native/global/user
scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding
skills, tools, tasks, processes and extensions as plain files.

- tasks move from Postgres to TASK.md files (new file-backed task layer);
  task editing now works, which the DB path never supported
- skills/tools/processes collapse into one shared file router (single dir)
- remove the marketplace client (sync-marketplace/sync-version) and the
  boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store
- drop the dead tasks + vestigial skills/tools/processes/extensions +
  item_chats tables (migration 0004)
- one-time migration script exports DB tasks and consolidates disk items

Migration verified: all 6 tasks round-trip through the runtime parser
identically to their DB rows (pipeline steps, triggers, script impls and
agentic bodies all intact).

NOTE: not yet functionally tested end-to-end — every item (each task mode,
tool, skill, extension) still needs to be run/exercised in the app before
this is trusted. To be done manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:39:17 +00:00
co-authored by Claude Opus 4.8
parent 5fd3d4faac
commit f3492512ba
29 changed files with 2366 additions and 1357 deletions
+12 -28
View File
@@ -21,12 +21,8 @@ const PI_CONFIG_DIR = join(require('node:os').homedir(), '.pi', 'agent');
const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
const getHomeDirForRole = (email: string, role: string | null): string =>
role === 'Super Admin' && process.env.HOME_DIR ? process.env.HOME_DIR : getHomeDir(email);
const getGlobalSkillsDir = () => join(DATA_PATH, 'skills');
const getUserSkillsDir = (email: string) => join(DATA_PATH, email, 'skills');
const getGlobalExtensionsDir = () => join(DATA_PATH, 'extensions');
const getUserExtensionsDir = (email: string) => join(DATA_PATH, email, 'extensions');
const getGlobalToolsDir = () => join(DATA_PATH, 'tools');
const getUserToolsDir = (email: string) => join(DATA_PATH, email, 'tools');
const OFFICER_ITEMS_DIR = process.env.OFFICER_ITEMS_DIR ?? join(process.cwd(), 'officer-items');
const itemsDir = (type: 'skills' | 'tools' | 'extensions') => join(OFFICER_ITEMS_DIR, type);
function isPidAlive(pid: number): boolean {
try {
@@ -97,11 +93,8 @@ function collectSkillFlagsFromDir(scanDir: string, targetDir: string): string[]
return flags;
}
function collectSkillFlags(email: string): string[] {
return [
...collectSkillFlagsFromDir(getGlobalSkillsDir(), getGlobalSkillsDir()),
...collectSkillFlagsFromDir(getUserSkillsDir(email), getUserSkillsDir(email)),
];
function collectSkillFlags(): string[] {
return collectSkillFlagsFromDir(itemsDir('skills'), itemsDir('skills'));
}
function collectExtensionFlagsFromDir(scanDir: string, targetDir: string): string[] {
@@ -118,11 +111,8 @@ function collectExtensionFlagsFromDir(scanDir: string, targetDir: string): strin
return flags;
}
function collectExtensionFlags(email: string): string[] {
return [
...collectExtensionFlagsFromDir(getGlobalExtensionsDir(), getGlobalExtensionsDir()),
...collectExtensionFlagsFromDir(getUserExtensionsDir(email), getUserExtensionsDir(email)),
];
function collectExtensionFlags(): string[] {
return collectExtensionFlagsFromDir(itemsDir('extensions'), itemsDir('extensions'));
}
async function resolveApiKeyForModel(model: string): Promise<string | null> {
@@ -278,17 +268,11 @@ export async function spawnPi(options: PiSpawnOptions): Promise<void> {
const isSuperAdmin = role === 'Super Admin';
const skillFlags = isSuperAdmin
? collectSkillFlags(email)
: [
...collectSkillFlagsFromDir(getGlobalSkillsDir(), SANDBOX_GLOBAL_SKILLS),
...collectSkillFlagsFromDir(getUserSkillsDir(email), `${SANDBOX_DATA}/skills`),
];
? collectSkillFlags()
: collectSkillFlagsFromDir(itemsDir('skills'), SANDBOX_GLOBAL_SKILLS);
const extensionFlags = isSuperAdmin
? collectExtensionFlags(email)
: [
...collectExtensionFlagsFromDir(getGlobalExtensionsDir(), SANDBOX_GLOBAL_EXTENSIONS),
...collectExtensionFlagsFromDir(getUserExtensionsDir(email), `${SANDBOX_DATA}/extensions`),
];
? collectExtensionFlags()
: collectExtensionFlagsFromDir(itemsDir('extensions'), SANDBOX_GLOBAL_EXTENSIONS);
const piArgs = [
...PI_CMD,
@@ -311,7 +295,7 @@ export async function spawnPi(options: PiSpawnOptions): Promise<void> {
}
const homeDir = getHomeDirForRole(email, role);
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
const toolsDirs = itemsDir('tools');
// Per-session JWT so tools (e.g. the gmail proxy) can call back to dev-platform
// as the owning user. Mirrors the signin payload shape so userMiddleware accepts it.
@@ -338,7 +322,7 @@ export async function spawnPi(options: PiSpawnOptions): Promise<void> {
proc = Bun.spawn(piArgs, { cwd, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', env });
} else {
// Non-admin: run inside bwrap sandbox
const sandboxToolsDirs = [SANDBOX_GLOBAL_TOOLS, `${SANDBOX_DATA}/tools`].join(':');
const sandboxToolsDirs = SANDBOX_GLOBAL_TOOLS;
const prefix = buildSandboxPrefix(email);
// Pi-specific env vars
+4 -3
View File
@@ -10,6 +10,7 @@ const BUN_DIR = (() => {
const PROJECT_ROOT = resolve(import.meta.dir, '../../..');
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
const OFFICER_ITEMS_DIR = process.env.OFFICER_ITEMS_DIR ?? join(process.cwd(), 'officer-items');
const HOST_HOME = process.env.HOME!;
// Resolve the OS username for runuser to drop privileges inside the sandbox
@@ -31,9 +32,9 @@ export const SANDBOX_GLOBAL_TOOLS = `${SANDBOX_GLOBAL_ROOT}/tools`;
// Callers can append extra `--setenv` args before calling `buildRunuserSuffix()`.
export function buildSandboxPrefix(email: string): string[] {
const userDataDir = join(DATA_PATH, email);
const globalSkillsDir = join(DATA_PATH, 'skills');
const globalToolsDir = join(DATA_PATH, 'tools');
const globalExtensionsDir = join(DATA_PATH, 'extensions');
const globalSkillsDir = join(OFFICER_ITEMS_DIR, 'skills');
const globalToolsDir = join(OFFICER_ITEMS_DIR, 'tools');
const globalExtensionsDir = join(OFFICER_ITEMS_DIR, 'extensions');
const args = [
'sudo',