Merge pull request #665 from rowboatlabs/feature/apps-v1

Rowboat Apps V1 — M1 + M2 (spec implementation)
This commit is contained in:
gagan 2026-07-06 14:18:11 +05:30 committed by GitHub
commit b7d1019538
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 3332 additions and 1317 deletions

View file

@ -33,6 +33,9 @@ export type BackgroundTask = {
projectId?: string;
model?: string;
provider?: string;
// Folder slug of the Rowboat App that installed this task (spec §8.2).
// Runtime-managed; tasks with sourceApp are owned by the app lifecycle.
sourceApp?: string;
createdAt: string;
// Runtime-managed — never hand-write. Mirrors live-note's flat-field
// pattern: `lastAttemptAt` is bumped at every run start (backoff anchor),
@ -53,6 +56,7 @@ export type BackgroundTaskSummary = {
active: boolean;
triggers?: Triggers;
projectId?: string;
sourceApp?: string;
createdAt: string;
lastAttemptAt?: string;
lastRunId?: string;
@ -71,6 +75,7 @@ export const BackgroundTaskSchema = z.object({
projectId: z.string().optional().describe('When set, marks this as a coding task pinned to a registered code project (repo). The agent implements detected work via the launch-code-task tool, each launch in its own isolated worktree.'),
model: z.string().optional().describe('ADVANCED — leave unset. Per-task model override.'),
provider: z.string().optional().describe('ADVANCED — leave unset. Per-task provider name override.'),
sourceApp: z.string().optional().describe('Folder slug of the app that installed this task. Runtime-managed.'),
createdAt: z.string().describe('ISO timestamp set once at create-time.'),
lastAttemptAt: z.string().optional().describe('Runtime-managed — never write this yourself. Bumped at the start of every agent run; used by the scheduler for backoff so failures do not retry-storm.'),
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself. The id of the most recent run (success or failure); used by the bg-task:stop handler.'),
@ -86,6 +91,7 @@ export const BackgroundTaskSummarySchema = z.object({
active: z.boolean(),
triggers: TriggersSchema.optional(),
projectId: z.string().optional(),
sourceApp: z.string().optional(),
createdAt: z.string(),
lastAttemptAt: z.string().optional(),
lastRunId: z.string().optional(),

View file

@ -20,4 +20,5 @@ export * as billing from './billing.js';
export * as notificationSettings from './notification-settings.js';
export * as codeSessions from './code-sessions.js';
export * as channels from './channels.js';
export * as rowboatApp from './rowboat-app.js';
export { PrefixLogger };

View file

@ -18,6 +18,7 @@ import { RequestedAgent, type TurnEvent } from './turns.js';
import type { SessionBusEvent, SessionIndexEntry, SessionState } from './sessions.js';
import { RowboatApiConfig } from './rowboat-account.js';
import { ZListToolkitsResponse } from './composio.js';
import { AppSummarySchema } from './rowboat-app.js';
import { BrowserStateSchema, HttpAuthRequestSchema } from './browser-control.js';
import { BillingInfoSchema } from './billing.js';
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
@ -1226,6 +1227,35 @@ const ipcSchemas = {
shouldShow: z.boolean(),
}),
},
// Rowboat Apps (spec §13) — M1 local channels.
'apps:list': {
req: z.object({}),
res: z.object({
serverRunning: z.boolean(),
serverError: z.string().optional(),
apps: z.array(AppSummarySchema),
}),
},
'apps:get': {
req: z.object({ folder: z.string() }),
res: z.object({
app: AppSummarySchema,
readme: z.string().optional(),
rollbackAvailable: z.boolean(),
}),
},
'apps:create': {
req: z.object({ folder: z.string(), name: z.string(), description: z.string() }),
res: z.object({ app: AppSummarySchema }),
},
'apps:delete': {
req: z.object({ folder: z.string() }),
res: z.object({ ok: z.literal(true) }),
},
'apps:setTheme': {
req: z.object({ theme: z.enum(['light', 'dark']) }),
res: z.object({ ok: z.literal(true) }),
},
'composio:didConnect': {
req: z.object({
toolkitSlug: z.string(),
@ -1239,6 +1269,34 @@ const ipcSchemas = {
req: z.object({}),
res: ZListToolkitsResponse,
},
// Mini Apps: execute a Composio tool by slug (scoped to a connected toolkit).
'composio:execute-tool': {
req: z.object({
toolkitSlug: z.string(),
toolSlug: z.string(),
arguments: z.record(z.string(), z.unknown()).optional(),
}),
res: z.object({
successful: z.boolean(),
data: z.unknown().optional(),
error: z.string().optional(),
}),
},
// Mini Apps: search Composio tools within a toolkit (returns slugs + schemas).
'composio:search-tools': {
req: z.object({
toolkitSlug: z.string(),
query: z.string(),
}),
res: z.object({
tools: z.array(z.object({
slug: z.string(),
name: z.string(),
description: z.string().optional(),
})),
error: z.string().optional(),
}),
},
// Agent schedule channels
'agent-schedule:getConfig': {
req: z.null(),

View file

@ -0,0 +1,113 @@
import { z } from 'zod';
// Rowboat Apps schemas (spec §4.2, §5.1, §9.1, §11.4, §12.2).
export const PACKAGE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
export const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
// ---------------------------------------------------------------------------
// Manifest — rowboat-app.json (§4.2)
// ---------------------------------------------------------------------------
export const RowboatAppManifestSchema = z.object({
schemaVersion: z.literal(1),
name: z.string().min(3).max(64).regex(PACKAGE_NAME_RE)
.describe('Package identity. Globally unique and immutable once published.'),
version: z.string().regex(SEMVER_RE)
.describe('Strict semver, no prerelease/build suffix in V1.'),
description: z.string().max(500).default(''),
icon: z.string().optional()
.describe('Path relative to dist/. Same traversal rules as entry.'),
entry: z.string().default('index.html')
.describe('Path relative to dist/. Serves as app root and SPA fallback.'),
agents: z.array(z.string().regex(/^[a-z0-9][a-z0-9-_]*\.yaml$/)).default([])
.describe('Filenames under agents/. Each must exist in the package.'),
capabilities: z.array(z.string()).default([])
.describe('Capability identifiers this app may use (D7): Composio toolkit slugs for /_rowboat/tools/*, plus the reserved identifiers "llm" (§7.6) and "copilot" (§7.7). Empty = none.'),
dataContracts: z.array(z.object({
file: z.string(), // path relative to data/, e.g. "data.json"
requiredKeys: z.array(z.string()).default([]),
nonEmptyArrayKeys: z.array(z.string()).default([]),
})).default([])
.describe('Write guards for specific data/ files: required top-level keys and keys that must stay non-empty arrays. Enforced on writes (§7.3, §8.6) so a buggy agent run cannot corrupt the app\'s data shape or wipe good series with empties.'),
// RESERVED — validated if present, ignored by V1 runtime:
build: z.object({ command: z.string() }).optional()
.describe('RESERVED. Rowboat MUST NOT execute this in V1.'),
minRowboatVersion: z.string().regex(SEMVER_RE).optional()
.describe('RESERVED. Minimum compatible Rowboat version; not enforced in V1.'),
}).passthrough(); // unknown fields survive round-trips (forward compatibility)
export type RowboatAppManifest = z.infer<typeof RowboatAppManifestSchema>;
// ---------------------------------------------------------------------------
// Install record — .rowboat-install.json (§12.2)
// ---------------------------------------------------------------------------
export const AppInstallRecordSchema = z.object({
name: z.string(),
repo: z.string().optional(), // owner/repo at install time; absent only for non-GitHub URL installs (§12.5)
sourceUrl: z.string().optional(), // present only for §12.5 URL installs
version: z.string(),
sha256: z.string(), // bundle checksum, pinned at install
installedAt: z.string(),
updatedAt: z.string().optional(),
files: z.record(z.string(), z.string()), // relpath → sha256 of release-managed files
previousVersion: z.string().optional(), // set while .previous/ exists
});
export type AppInstallRecord = z.infer<typeof AppInstallRecordSchema>;
// ---------------------------------------------------------------------------
// Publish record — .rowboat-publish.json (§11.4)
// ---------------------------------------------------------------------------
export const AppPublishRecordSchema = z.object({
name: z.string(),
login: z.string(), // publisher GitHub login
repo: z.string(), // owner/repo
lastPublishedVersion: z.string().optional(),
lastSha256: z.string().optional(),
pendingSteps: z.object({ // present only mid-publish (resume state)
version: z.string(),
completed: z.array(z.string()), // step names from §11.2
releaseId: z.number().optional(),
prUrl: z.string().optional(),
}).optional(),
});
export type AppPublishRecord = z.infer<typeof AppPublishRecordSchema>;
// ---------------------------------------------------------------------------
// App summary — apps:list / apps:get (§5.1)
// ---------------------------------------------------------------------------
export const AppSummarySchema = z.object({
folder: z.string(), // folder slug
status: z.enum(['ok', 'invalid']),
manifest: RowboatAppManifestSchema.optional(), // present when ok
manifestError: z.string().optional(), // present when invalid
origin: z.string(), // http://<folder>.apps.localhost:3210
kind: z.enum(['local', 'installed']),
install: AppInstallRecordSchema.optional(), // §12.2
publish: AppPublishRecordSchema.optional(), // §11.4
hasDist: z.boolean(),
agentSlugs: z.array(z.string()), // materialized bg-task slugs (§8.3)
});
export type AppSummary = z.infer<typeof AppSummarySchema>;
// ---------------------------------------------------------------------------
// Registry record — apps/<name>.json in the registry repo (§9.1)
// ---------------------------------------------------------------------------
export const RegistryRecordSchema = z.object({
schemaVersion: z.literal(1),
name: z.string().min(3).max(64).regex(PACKAGE_NAME_RE),
owner: z.string().min(1), // GitHub login of the publisher
repo: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/), // "owner/repo"
description: z.string().max(500).default(''),
iconUrl: z.string().url().optional(), // https URL for the catalog listing icon
createdAt: z.string(), // ISO 8601
}).strict();
export type RegistryRecord = z.infer<typeof RegistryRecordSchema>;

View file

@ -32,6 +32,8 @@ export const StartEvent = BaseRunEvent.extend({
"meeting_note",
"knowledge_sync",
"code_session",
"app_llm_generate",
"app_copilot_run",
]).optional(),
subUseCase: z.string().optional(),
});
@ -202,6 +204,8 @@ export const UseCase = z.enum([
"meeting_note",
"knowledge_sync",
"code_session",
"app_llm_generate",
"app_copilot_run",
]);
export const Run = z.object({