Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
4072b4e
fix: decode and stop silently truncating inbound integration messages
ahmad-ajmal Sep 30, 2026
3a0089d
Fix posthog proxy destination issue and agent app bidirectional commu…
Sep 30, 2026
0d19ea0
clean comments
ahmad-ajmal Sep 30, 2026
e6d0d7f
clean up comments
ahmad-ajmal Sep 30, 2026
dc95e6f
agent recieves truncated information
ahmad-ajmal Sep 30, 2026
68e3aef
Provide LLM reasoning effort param
Oct 1, 2026
de5902c
create effort setting knob
Oct 2, 2026
10ffd5d
fix max token ignores the reasoning output cap bug
Oct 2, 2026
0c054e1
openai setting update
Oct 2, 2026
fb3f785
fix duplicated steps and second decision per request
Oct 2, 2026
c553f08
fix incoming message encoding issue
Oct 2, 2026
af26ca4
Merge pull request #467 from CraftOS-dev/ahmad/system-message-format
zfoong Oct 2, 2026
991fe15
very minor UI update
Oct 2, 2026
c86c275
make gmail able to send inline email
zfoong Oct 5, 2026
236eff6
Sidebar UI update
zfoong Oct 5, 2026
ffb6b3b
Fix default reasoning effort issue when switching model
Oct 5, 2026
22637aa
Merge pull request #469 from CraftOS-dev/llm-reasoning-effort-param
zfoong Oct 5, 2026
6e0be02
supabase integration
ahmad-ajmal Oct 5, 2026
077a2c1
discard action sets umbrella of and describe set function
Oct 5, 2026
bba760c
Merge pull request #472 from CraftOS-dev/ahmad/supabase-integration
zfoong Oct 5, 2026
19dd12d
figma integrations
ahmad-ajmal Oct 6, 2026
6133a69
docs: add Figma integration result screenshots
ahmad-ajmal Oct 6, 2026
6f90c00
allow agent app's iframe to enable device access
Oct 6, 2026
ae18c5f
fix tunnel failed silently and get error from API rather than log file
Oct 7, 2026
a49baf2
fix: allow cloudflared time to finish edge retries
ahmad-ajmal Oct 7, 2026
4ba74de
Merge pull request #474 from CraftOS-dev/fix-cloudflare-silent-error
CraftOS-dev Oct 7, 2026
587e9ec
fix #475: guide Windows shell execution and report quoting hazards
ahmad-ajmal Oct 7, 2026
c27d142
fix #475: scope shell guidance and preserve native errors
ahmad-ajmal Oct 8, 2026
2951c8b
feat(mini-browser): import the Web Agent browser prototype onto main
korivi-CraftOS Oct 9, 2026
90e2d7f
feat(mini-browser): rebuild the Web Agent prototype as the Mini Browser
korivi-CraftOS Oct 9, 2026
ef63c72
fix(mini-browser): address the multi-agent review (53 confirmed findi…
korivi-CraftOS Oct 9, 2026
83c73dd
feat #479: add interactive Generative UI outputs
ahmad-ajmal Oct 9, 2026
c563ed7
update logs accumulation issue
Oct 11, 2026
53394f6
feat #479: improve Generative UI workspace UX and motion
ahmad-ajmal Oct 12, 2026
cf032e8
refactor #479: shorten Generative UI action instructions
ahmad-ajmal Oct 12, 2026
7baef3d
fix: add agent app marketplace repository fallback
ahmad-ajmal Oct 12, 2026
e6849a1
Merge pull request #473 from CraftOS-dev/ahmad/figma-integration
zfoong Oct 12, 2026
fb5df85
added run powershell action for windows only to prevent run shell enc…
Oct 13, 2026
77ac1ec
fix #475: reject non-cmd Windows shell selections
ahmad-ajmal Oct 13, 2026
9c7d20a
Merge pull request #481 from CraftOS-dev/ahmad/agent-app-fallback
zfoong Oct 13, 2026
0bff145
fix: enable selected-contact Telegram bot auto-replies
ahmad-ajmal Oct 13, 2026
2c03083
Merge V1.4.4 into Telegram auto-reply branch
ahmad-ajmal Oct 13, 2026
2217549
fix: authenticate Telegram owners and preserve reply context with typ…
ahmad-ajmal Oct 14, 2026
2c9ff84
fix: resolve short owner replies to quoted Telegram requests
ahmad-ajmal Oct 14, 2026
cfdc1b9
fix: enforce Telegram conversation-only tools before dispatch
ahmad-ajmal Oct 14, 2026
63a84e8
fix(ui): prevent integration settings loader from hanging
ahmad-ajmal Oct 14, 2026
ccf5237
fix(mini-browser): address the PR #483 review
korivi-CraftOS Oct 14, 2026
d232fcc
fix #475: allow PowerShell validation checks
ahmad-ajmal Oct 14, 2026
043c868
Merge pull request #477 from CraftOS-dev/ahmad/fix-windows-shell-guid…
zfoong Oct 14, 2026
fe56335
grep improvement update using ripgrep
Oct 15, 2026
4b136d1
Make agent aware of NOTE and make missions obsolete
Oct 15, 2026
3b3612f
Merge pull request #485 from CraftOS-dev/ahmad/integration-loader
zfoong Oct 15, 2026
1447bd3
Merge pull request #487 from CraftOS-dev/improvement/notes-and-missio…
CraftOS-dev Oct 15, 2026
4332241
Fixed action set accumulation issue causing system prompt to bloat
Oct 15, 2026
be5f4c2
fix: generalize message authority and quoted-reply interpretation
ahmad-ajmal Oct 15, 2026
261366b
Merge pull request #482 from CraftOS-dev/ahmad/telegram-contact-auto-…
zfoong Oct 15, 2026
dadd733
read and decode rg's plan output instead of json
Oct 15, 2026
f067426
Merge pull request #486 from CraftOS-dev/fix/grep-improvement
zfoong Oct 15, 2026
ccd696a
fix(mini-browser): stop the post-box retype loop; let pages play sound
korivi-CraftOS Oct 15, 2026
fb83480
fix(mini-browser): show whole posts in observations, never half a link
korivi-CraftOS Oct 15, 2026
d9800ff
fix(mini-browser): address the PR #483 re-review
korivi-CraftOS Oct 15, 2026
aad61fc
fix: preserve integration guidance for action subgroups
ahmad-ajmal Oct 15, 2026
eb8af8b
fix(mini-browser): address the third PR #483 review
korivi-CraftOS Oct 15, 2026
8d154d1
fix: change google scopes
ahmad-ajmal Oct 15, 2026
65353e0
Merge pull request #489 from CraftOS-dev/fix/derived-action-sets
CraftOS-dev Oct 15, 2026
a54b169
Merge pull request #490 from CraftOS-dev/ahmad/google-oauth-scope-ali…
CraftOS-dev Oct 15, 2026
92a9ec2
fix browser interface launching delay issue
Oct 16, 2026
39734e9
Merge branch 'V1.4.4' into ahmad/generative-ui
zfoong Oct 16, 2026
295dbe2
Merge pull request #480 from CraftOS-dev/ahmad/generative-ui
zfoong Oct 16, 2026
47edcc8
fix requirement repeating in event stream and frequent folding issue
Oct 16, 2026
533413e
Merge branch 'V1.4.4' of https://github.com/craftos-dev/craftbot into…
Oct 16, 2026
f9977ff
Merge pull request #483 from CraftOS-dev/feature/mini-browser
Oct 16, 2026
c3fc491
major UI update to incoporate session based browser and generative UI
Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ app/config/settings.json
app/config/mcp_config.json
app/config/skills_config.json
app/config/scheduler_config.json
# Mini Browser's Chromium profile: cookies and signed-in sessions, plus its
# lock file (its password vault is in .credentials, above).
**/mini_browser_profile
**/mini_browser_profile.lock

# Per-install agent state
app/data/.usage
Expand Down
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ OMNIPARSER_BASE_URL=

# Google (/google login)
GOOGLE_CLIENT_ID=
# Optional Figma OAuth app credentials. PAT connection works without these.
# Public apps need Figma approval; do not commit real client secrets.
FIGMA_CLIENT_ID=
FIGMA_CLIENT_SECRET=
GOOGLE_CLIENT_SECRET=

# LinkedIn (/linkedin login)
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/windows-shell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Windows shell regressions

on:
push:
paths:
- 'app/data/action/run_shell.py'
- 'app/data/action/run_powershell.py'
- 'tests/windows_shell/**'
- '.github/workflows/windows-shell.yml'
pull_request:
paths:
- 'app/data/action/run_shell.py'
- 'app/data/action/run_powershell.py'
- 'tests/windows_shell/**'
- '.github/workflows/windows-shell.yml'

jobs:
shell:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Verify real cmd and Windows PowerShell execution
run: python -m unittest discover -s tests/windows_shell -v
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ agent_bundle/
**/.craftbot/
app/data/.file_index/
.playwright-mcp
**/test-results/
**/playwright-report/
# Sidecar Node runtime (install.py downloads it when the system Node is too old for Agent App)
runtime/

Expand All @@ -85,3 +87,8 @@ npm-cache/
playwright-browsers/

hf-cache/

# Mini Browser's persistent Chromium profiles, one per chat (cookies,
# signed-in sessions), and their cross-process lock files. Its password
# vault lives in .credentials/ (ignored above). Never commit.
**/mini_browser_profile*
14 changes: 12 additions & 2 deletions agent-app/blueprint/pb/pb_hooks/_craftbot_bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* const bridge = require(`${__hooks}/_craftbot_bridge.js`);
* const text = bridge.callLLM('Summarize: ...', 'You are terse.');
* const obj = JSON.parse(bridge.callLLM(prompt, system, { json: true }) || 'null');
* const res = bridge.callAction('send_gmail', { to, subject, body });
* const raw = bridge.callIntegration('slack', 'POST', '/chat.postMessage', { ... });
*
Expand All @@ -14,15 +15,24 @@
* real paths/payloads there). All no-op gracefully outside CraftBot.
*/

function callLLM(prompt, systemMessage) {
/**
* options.json: the provider's JSON mode constrains the reply to one JSON
* object (the prompt must still describe the object's shape). Use it whenever
* the hook JSON.parse()s the reply, never for prose.
*/
function callLLM(prompt, systemMessage, options) {
try {
const bridge = $os.getenv('CRAFTBOT_BRIDGE_URL');
const token = $os.getenv('CRAFTBOT_BRIDGE_TOKEN');
if (!bridge || !token) return '';
const res = $http.send({
url: bridge + '/api/bridge/llm',
method: 'POST',
body: JSON.stringify({ prompt: prompt, system_message: systemMessage || '' }),
body: JSON.stringify({
prompt: prompt,
system_message: systemMessage || '',
json_mode: !!(options && options.json),
}),
headers: {
'content-type': 'application/json',
authorization: 'Bearer ' + token,
Expand Down
3 changes: 3 additions & 0 deletions agent-app/docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ system module `pb/pb_hooks/_craftbot_bridge.js` — no API keys in the app:
```js
const bridge = require(`${__hooks}/_craftbot_bridge.js`);
const summary = bridge.callLLM('Summarize:\n' + text, 'Reply in one sentence.');
// Parsing the reply? Ask for the provider's JSON mode instead of scraping prose:
const notes = JSON.parse(bridge.callLLM(prompt, 'Reply with one JSON object.', { json: true }) || 'null');
const res = bridge.callIntegration('slack', 'POST', '/chat.postMessage', { ... });
```

Expand Down Expand Up @@ -175,6 +177,7 @@ Use the CLI — it resolves the port, authenticates, and validates params:

```
agent-app ops <project> # what can this app do?
agent-app ops <project> <area> # one area's ops, every param explained
agent-app run <project> <op> --param value # execute a declared op
agent-app data <project> <collection> list --filter '...' --limit 20
agent-app data <project> <collection> create --json '{...}'
Expand Down
8 changes: 6 additions & 2 deletions agent-app/tools/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ async function bootstrapSuperuser(projectDir: string): Promise<void> {
const email = 'agent@agent-app.local';
const password = randomBytes(18).toString('base64url');

// Flags first, then `--`, then email and password: a base64url password
// can start with "-", which PocketBase's CLI would otherwise parse as an
// unknown flag (it then saves nothing and still exits 0).
execFileSync(
pbBin,
[
'superuser',
'upsert',
email,
password,
'--dir',
join(pbDir, 'pb_data'),
'--migrationsDir',
join(pbDir, 'pb_migrations'),
'--hooksDir',
join(pbDir, 'pb_hooks'),
'--',
email,
password,
],
{ stdio: 'pipe' },
);
Expand Down
30 changes: 24 additions & 6 deletions agent-app/tools/src/commands/ops.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
/**
* agent-app ops <project-dir> — the app's declared verb surface (spec O1/O4).
* The agent-facing capability card: what this app can DO.
* agent-app ops <project-dir> [area]: the app's declared verb surface (spec O1/O4).
* The agent-facing capability card: what this app can DO. With an area (an op
* name or the part before its last dot, e.g. `notes` or `notes.create`) it lists
* only those ops, each param with its description, so an agent can look up
* one area of a large app without reading every op.
*/
import { log } from '../lib/log.ts';
import { loadOps, loadProject } from '../lib/project.ts';

export async function run(args: string[]): Promise<number> {
const dirArg = args.find((a) => !a.startsWith('--'));
const [dirArg, area] = args.filter((a) => !a.startsWith('--'));
if (dirArg === undefined) {
log.error('Usage: agent-app ops <project-dir>');
log.error('Usage: agent-app ops <project-dir> [area]');
return 1;
}
const project = loadProject(dirArg);
const ops = loadOps(project);
const all = loadOps(project);
const ops = area === undefined ? all : all.filter((o) => o.name === area || o.name.startsWith(area + '.'));
if (ops.length === 0) {
const areas = [...new Set(all.map((o) => o.name.slice(0, Math.max(o.name.lastIndexOf('.'), 0)) || o.name))];
log.error(`No op matches "${area}". Areas: ${areas.join(', ')}`);
return 1;
}

log.raw(`${project.name} (${project.id}) — ${project.baseUrl}\n`);
for (const op of ops) {
const params = Object.entries(op.params ?? {})
.map(([k, v]) => `--${k} <${v.type}>${v.required ? '' : '?'}`)
.map(([k, v]) => `--${k} <${v.enum ? v.enum.join('|') : v.type}>${v.required ? '' : '?'}`)
.join(' ');
const flags = [op.system ? 'system' : '', op.destructive ? 'DESTRUCTIVE' : '']
.filter(Boolean)
.join(', ');
log.raw(` ${op.name}${params ? ' ' + params : ''}${flags ? ` [${flags}]` : ''}`);
log.raw(` ${op.description}`);
if (area !== undefined) {
for (const [k, v] of Object.entries(op.params ?? {})) {
if (v.description) log.raw(` --${k}: ${v.description}`);
}
}
}
log.raw(`\nRun one: agent-app run ${dirArg} <op-name> [--param value ...]`);
const example = ops.find((o) => !o.system)?.name;
if (area === undefined && example !== undefined) {
log.raw(`Details: agent-app ops ${dirArg} <area> (each param explained, e.g. ${example.slice(0, example.lastIndexOf('.'))})`);
}
log.raw(`Data: agent-app data ${dirArg} <collection> [list|get <id>|create|update <id>|delete <id>] [--json '{...}'] [--filter '...']`);
return 0;
}
26 changes: 18 additions & 8 deletions agent-app/tools/src/lib/os-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,31 @@ export class OSAdapter {
this.arch = arch === 'arm64' ? 'arm64' : 'amd64';
}

/** Per-user cache root of the agent-app tooling. */
private cacheRoot(): string {
return {
darwin: join(homedir(), 'Library', 'Caches', 'craftos-agent-app'),
linux: join(homedir(), '.cache', 'craftos-agent-app'),
win32: join(process.env['LOCALAPPDATA'] ?? join(homedir(), 'AppData', 'Local'), 'craftos-agent-app'),
}[this.platform];
}

/** Central per-host PocketBase binary cache, versioned (spec B1). */
pbCacheDir(version: string): string {
const override = process.env['AGENT_APP_PB_CACHE'];
const base =
override ??
{
darwin: join(homedir(), 'Library', 'Caches', 'craftos-agent-app', 'pb'),
linux: join(homedir(), '.cache', 'craftos-agent-app', 'pb'),
win32: join(process.env['LOCALAPPDATA'] ?? join(homedir(), 'AppData', 'Local'), 'craftos-agent-app', 'pb'),
}[this.platform];
const base = process.env['AGENT_APP_PB_CACHE'] ?? join(this.cacheRoot(), 'pb');
const dir = join(base, version);
mkdirSync(dir, { recursive: true });
return dir;
}

/** Where the CLI keeps each app's superuser session between commands. Kept
* out of the project folder so it never travels with an exported app. */
cliSessionDir(): string {
const dir = join(this.cacheRoot(), 'cli-sessions');
mkdirSync(dir, { recursive: true });
return dir;
}

/** GitHub release asset name for the pinned version. */
pbAssetName(version: string): string {
const os = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[this.platform];
Expand Down
75 changes: 69 additions & 6 deletions agent-app/tools/src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/** Operate-command helpers: resolve a project, its port, ops, and auth. */
import { existsSync, readFileSync } from 'node:fs';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';

import { osAdapter } from './os-adapter.ts';

export interface ProjectRef {
dir: string;
name: string;
Expand Down Expand Up @@ -84,7 +86,7 @@ export interface Operation {
description: string;
system?: boolean;
destructive?: boolean;
params?: Record<string, { type: string; description?: string; required?: boolean }>;
params?: Record<string, { type: string; description?: string; required?: boolean; enum?: (string | number)[] }>;
executor: { type: string; method?: string; path?: string; collection?: string; action?: string };
}

Expand All @@ -104,17 +106,72 @@ export function readAgentToken(project: ProjectRef): string | null {
}

/** Superuser token via the project-local .superuser file (absent on imports). */
export async function authToken(project: ProjectRef): Promise<string | null> {
/**
* Superuser session for projects that have a `.superuser` file. A failed
* sign-in is an error, never a silent fallback: carrying on unauthenticated
* turned protected reads into a bare "not found", which agents took to mean
* "already handled by someone else" (IP Manager, 2026-09-29).
*/
export async function authToken(project: ProjectRef, fresh = false): Promise<string | null> {
const credFile = join(project.dir, '.superuser');
if (!existsSync(credFile)) return null;
if (!fresh) {
const cached = cachedSession(project);
if (cached !== null) return cached;
}
const { email, password } = JSON.parse(readFileSync(credFile, 'utf8'));
const res = await fetch(`${project.baseUrl}/api/collections/_superusers/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: email, password }),
});
if (!res.ok) return null;
return ((await res.json()) as { token: string }).token;
if (res.status === 429) {
throw new Error(
`Could not sign in to ${project.name}: PocketBase rate-limits sign-ins (HTTP 429, too many in the ` +
`last minute), so nothing was read or changed. Wait a minute, then retry.`,
);
}
if (!res.ok) {
throw new Error(
`Could not sign in to ${project.name} as its superuser (HTTP ${res.status}): the credentials in ` +
`.superuser do not match the app's database, so nothing was read or changed. ` +
`Relaunch the app from CraftBot to re-apply them, then retry.`,
);
}
const token = ((await res.json()) as { token: string }).token;
saveSession(project, token);
return token;
}

/**
* The superuser session is cached per app instance (id + port, so a shadow
* keeps its own) until shortly before it expires. Every CLI command is a new
* process, so without the cache each one signed in again, and PocketBase
* allows 30 sign-ins a minute: an agent working through a batch (filing 30
* SOPs, say) started failing mid-way.
*/
function sessionFile(project: ProjectRef): string {
return join(osAdapter.cliSessionDir(), `${project.id || 'app'}-${project.port}.json`);
}

function cachedSession(project: ProjectRef): string | null {
try {
const { token } = JSON.parse(readFileSync(sessionFile(project), 'utf8')) as { token: string };
const payload = JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8')) as {
exp?: number;
};
return typeof payload.exp === 'number' && payload.exp * 1000 > Date.now() + 60_000 ? token : null;
} catch {
return null;
}
}

function saveSession(project: ProjectRef, token: string): void {
try {
writeFileSync(sessionFile(project), JSON.stringify({ token }), { mode: 0o600 });
} catch {
/* no cache: the next command signs in again */
}
}

export async function request(
Expand Down Expand Up @@ -151,6 +208,12 @@ export async function request(
if (extraHeaders !== undefined) Object.assign(headers, extraHeaders);
const init: RequestInit = { method, headers };
if (body !== undefined) init.body = JSON.stringify(body);
const res = await fetch(`${project.baseUrl}${path}`, init);
let res = await fetch(`${project.baseUrl}${path}`, init);
if (res.status === 401 && token !== null) {
// A cached session the app no longer accepts (app data reset, secret
// rotated): sign in once more and repeat. A 401 means nothing was done.
headers['Authorization'] = (await authToken(project, true)) ?? '';
res = await fetch(`${project.baseUrl}${path}`, init);
}
return { status: res.status, body: await res.text() };
}
9 changes: 7 additions & 2 deletions agent_core/core/action_framework/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,13 @@ def list_all_actions_as_json(self) -> List[Dict[str, Any]]:
return json_actions_list

def find_action_by_name(self, action_name: str) -> Optional[Dict[str, Any]]:
"""Find an action by name and return its JSON representation."""
if action_name not in self._registry:
"""Find an action by name and return its JSON representation.

An action with no implementation for this OS does not exist here.
Action sets already leave it out of the prompt; this keeps a name the
model produces anyway from resolving to another OS's code.
"""
if self.get_action_implementation(action_name) is None:
return None

platform_impls = self._registry[action_name]
Expand Down
Loading
Loading