Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9c3a225
docs: update Cloudflare permissions for Developer Platform reorganiza…
lionello Apr 14, 2026
d72cd7a
docs: clarify CF permissions table with 4-column layout and fix quick…
simple-agent-manager[bot] Apr 14, 2026
d2c0cb5
docs: improve self-hosting guide clarity for new developers (#714)
simple-agent-manager[bot] Apr 14, 2026
edbac91
fix: use actual Pages subdomain for DNS CNAME instead of computed nam…
simple-agent-manager[bot] Apr 14, 2026
db5e89c
fix: pass Origin CA Key to Pulumi for OriginCaCertificate creation (#…
simple-agent-manager[bot] Apr 14, 2026
83d5edb
fix: add missing SSL permission for Origin CA — no separate key neede…
simple-agent-manager[bot] Apr 14, 2026
f0591f7
blog: SAM's journal — the Workers AI proxy rabbit hole (#730)
simple-agent-manager[bot] Apr 15, 2026
faf4dbe
task: add resource diagnostics for workspace build timeout
raphaeltm Apr 15, 2026
d6bcf78
feat: resource-aware diagnostics for workspace build timeouts (#731)
simple-agent-manager[bot] Apr 15, 2026
9f83e29
fix: migrate AI proxy config from secrets to wrangler vars (#732)
simple-agent-manager[bot] Apr 16, 2026
a52e3a2
feat: improve knowledge graph retrieval and agent instructions (#734)
simple-agent-manager[bot] Apr 16, 2026
9e70dd8
blog: SAM's journal — why VMs took 30 minutes to boot (#736)
simple-agent-manager[bot] Apr 16, 2026
83bdc3e
fix: OpenCode config for Workers AI proxy + remove Neko browser sidec…
simple-agent-manager[bot] Apr 16, 2026
f781250
fix: replace docker-in-docker with privileged mode in lightweight dev…
simple-agent-manager[bot] Apr 17, 2026
f097bd7
feat: node debug package download (#740)
simple-agent-manager[bot] Apr 17, 2026
961e83a
task: add recent chats dropdown for mobile + desktop nav
raphaeltm Apr 17, 2026
48818bb
feat: recent chats dropdown for quick chat switching (#741)
simple-agent-manager[bot] Apr 17, 2026
f290860
blog: SAM's journal — killing Docker-in-Docker (#743)
simple-agent-manager[bot] Apr 17, 2026
8bdf06f
Merge branch 'main' into fix-main
raphaeltm Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/deploy-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
MISSING="$MISSING\n - secrets.GH_APP_SLUG"
fi
if [ "$HAS_CF_ORIGIN_CA_KEY" != "true" ]; then
MISSING="$MISSING\n - secrets.CF_ORIGIN_CA_KEY (Cloudflare Origin CA Key — required for Origin CA certificates)"
echo "::notice::CF_ORIGIN_CA_KEY not set — using CF_API_TOKEN for Origin CA certs. If Pulumi fails with error 1016, ensure your API token has Zone > SSL and Certificates > Edit permission."
fi

if [ -n "$MISSING" ]; then
Expand Down Expand Up @@ -255,6 +255,14 @@ jobs:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
BASE_DOMAIN: ${{ vars.BASE_DOMAIN }}

- name: Configure AI Gateway
if: ${{ inputs.dry_run != true }}
run: bash scripts/deploy/configure-ai-gateway.sh
env:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
AI_GATEWAY_ID: ${{ vars.RESOURCE_PREFIX || 'sam' }}

- name: Check First Deploy Status
id: first_deploy
if: ${{ inputs.dry_run != true }}
Expand Down
160 changes: 160 additions & 0 deletions .playwright-mcp/page-2026-04-16T08-23-01-623Z.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e5]:
- img "SAM" [ref=e6]
- button "Notifications (31 unread)" [ref=e8] [cursor=pointer]:
- img [ref=e9]
- generic [ref=e12]: "31"
- button "Open command palette" [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic [ref=e17]: Search...
- generic [ref=e18]: Ctrl+K
- generic [ref=e20]:
- navigation "Project navigation" [ref=e21]:
- button "Show global navigation" [ref=e22] [cursor=pointer]:
- img [ref=e23]
- generic [ref=e25]: Back to Projects
- generic "elysia" [ref=e26]
- link "Chat" [ref=e27] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/chat
- img [ref=e28]
- text: Chat
- link "Library" [ref=e30] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/library
- img [ref=e31]
- text: Library
- link "Ideas" [ref=e33] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/ideas
- img [ref=e34]
- text: Ideas
- link "Knowledge" [ref=e36] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/knowledge
- img [ref=e37]
- text: Knowledge
- link "Notifications" [ref=e47] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/notifications
- img [ref=e48]
- text: Notifications
- link "Triggers" [ref=e51] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/triggers
- img [ref=e52]
- text: Triggers
- link "Profiles" [ref=e55] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/profiles
- img [ref=e56]
- text: Profiles
- link "Activity" [ref=e68] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/activity
- img [ref=e69]
- text: Activity
- link "Settings" [ref=e71] [cursor=pointer]:
- /url: /projects/01KJVGMWX26SGQ5DX94GMTJRQN/settings
- img [ref=e72]
- text: Settings
- navigation [ref=e75]:
- button [ref=e76] [cursor=pointer]:
- img [ref=e77]
- generic [ref=e79]: Back to elysia
- link [ref=e80] [cursor=pointer]:
- /url: /dashboard
- img [ref=e81]
- text: Home
- link [ref=e84] [cursor=pointer]:
- /url: /chats
- img [ref=e85]
- text: Chats
- link [ref=e87] [cursor=pointer]:
- /url: /projects
- img [ref=e88]
- text: Projects
- link [ref=e90] [cursor=pointer]:
- /url: /account-map
- img [ref=e91]
- text: Map
- link [ref=e93] [cursor=pointer]:
- /url: /settings
- img [ref=e94]
- text: Settings
- link [ref=e97] [cursor=pointer]:
- /url: /admin
- img [ref=e98]
- text: Admin
- button [ref=e101] [cursor=pointer]:
- img [ref=e102]
- text: Infrastructure
- generic [ref=e104]:
- img "serverspresentation2025" [ref=e105]
- generic [ref=e107]: serverspresentation2025
- button "Sign out" [ref=e108] [cursor=pointer]:
- img [ref=e109]
- main [ref=e111]:
- generic [ref=e113]:
- generic [ref=e114]:
- generic [ref=e115]:
- generic [ref=e116]: elysia
- button "Project status" [ref=e117] [cursor=pointer]:
- img [ref=e118]
- button "Automation triggers" [ref=e124] [cursor=pointer]:
- img [ref=e125]
- button "Project settings" [ref=e128] [cursor=pointer]:
- img [ref=e129]
- button "+ New Chat" [ref=e133] [cursor=pointer]
- generic [ref=e135]:
- img
- textbox "Search chats..." [ref=e136]
- navigation "Chat sessions" [ref=e137]:
- button "I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt... I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt... Active 3 msgs 6m ago" [ref=e139] [cursor=pointer]:
- 'generic "Idea: I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt..." [ref=e141]':
- img
- text: I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt...
- generic [ref=e145]: I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt...
- generic [ref=e146]:
- generic [ref=e147]: Active
- generic [ref=e148]: 3 msgs
- generic [ref=e149]: 6m ago
- button "Older (67)" [ref=e150] [cursor=pointer]:
- img [ref=e151]
- generic [ref=e153]: Older (67)
- generic [ref=e156]:
- generic [ref=e161]:
- generic: I am Gemma, a large language model created by the Gemma team at Google DeepMind. Create hello.txt...
- 'generic "Workspace profile: Lightweight" [ref=e162]': Lightweight
- link "4096" [ref=e164] [cursor=pointer]:
- /url: https://ws-01kpan8zpngxvsf7rjcmhvbbbw--4096.sammy.party
- img [ref=e165]
- text: "4096"
- generic [ref=e168]:
- button "Retry task" [ref=e169] [cursor=pointer]:
- img [ref=e170]
- button "Fork session" [ref=e173] [cursor=pointer]:
- img [ref=e174]
- generic [ref=e179]: Active
- button "Show session details" [ref=e181] [cursor=pointer]:
- img [ref=e182]
- log "Conversation" [ref=e184]:
- generic [ref=e192]:
- paragraph [ref=e193]: "Do these two things:"
- list [ref=e194]:
- listitem [ref=e195]:
- paragraph [ref=e196]: Tell me what AI model you are. State your model name and who made you.
- listitem [ref=e197]:
- paragraph [ref=e198]: "Create three files in the current directory:"
- list [ref=e199]:
- listitem [ref=e200]: hello.txt with the content "Hello from the AI agent"
- listitem [ref=e201]: info.md with a brief markdown document about yourself (2-3 sentences)
- listitem [ref=e202]: test.json with a simple JSON object containing your model name and the current date
- paragraph [ref=e203]: Do both tasks in a single response.
- separator [ref=e204]
- paragraph [ref=e205]:
- text: "IMPORTANT: Before starting any work, you MUST call the"
- code [ref=e206]: get_instructions
- text: tool from the sam-mcp MCP server. This provides your task context, project information, output branch name, and instructions for reporting progress. Do not proceed until you have called this tool and read its response.
- button "Scroll to bottom" [active] [ref=e207] [cursor=pointer]:
- img [ref=e208]
- generic [ref=e214]:
- button "Attach files" [ref=e215] [cursor=pointer]:
- img [ref=e216]
- textbox "Send a message..." [ref=e218]
- button "Start voice input" [ref=e219] [cursor=pointer]:
- img [ref=e220]
- button "Send" [disabled] [ref=e223]
4 changes: 4 additions & 0 deletions apps/api/src/durable-objects/project-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ export class ProjectData extends DurableObject<Env> {
return knowledge.getRelevantKnowledge(this.sql, context, limit);
}

async getAllHighConfidenceKnowledge(minConfidence: number, limit: number) {
return knowledge.getAllHighConfidenceKnowledge(this.sql, minConfidence, limit);
}

async createKnowledgeRelation(sourceEntityId: string, targetEntityId: string, relationType: string, description: string | null) {
const result = knowledge.createRelation(this.sql, sourceEntityId, targetEntityId, relationType as Parameters<typeof knowledge.createRelation>[3], description);
this.broadcastEvent('knowledge.relation.created', { id: result.id });
Expand Down
23 changes: 23 additions & 0 deletions apps/api/src/durable-objects/project-data/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,29 @@ export function getRelevantKnowledge(sql: SqlStorage, context: string, limit: nu
return rows.map(parseKnowledgeObservationSearchRow);
}

/**
* Get ALL active observations with confidence >= threshold, ordered by entity
* then recency. Used for session-start knowledge injection — returns everything
* important rather than trying to guess relevance from keywords.
*/
export function getAllHighConfidenceKnowledge(
sql: SqlStorage,
minConfidence: number,
limit: number,
) {
const rows = sql.exec(
`SELECT o.*, e.name as entity_name, e.entity_type
FROM knowledge_observations o
JOIN knowledge_entities e ON e.id = o.entity_id
WHERE o.is_active = 1 AND o.confidence >= ?
ORDER BY e.name, o.last_confirmed_at DESC
LIMIT ?`,
minConfidence, limit,
).toArray();

return rows.map(parseKnowledgeObservationSearchRow);
}

// ─── Relations ──────────────────────────────────────────────────────────────

export function createRelation(
Expand Down
9 changes: 4 additions & 5 deletions apps/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export interface Env {
KNOWLEDGE_MAX_OBSERVATIONS_PER_ENTITY?: string; // Max observations per entity (default: 100)
KNOWLEDGE_SEARCH_LIMIT?: string; // Max search results (default: 20)
KNOWLEDGE_AUTO_RETRIEVE_LIMIT?: string; // Max auto-retrieved observations on session start (default: 20)
KNOWLEDGE_AUTO_RETRIEVE_MIN_CONFIDENCE?: string; // Min confidence for auto-retrieved observations (default: 0.8)
KNOWLEDGE_AUTO_RETRIEVE_HIGH_CONFIDENCE_LIMIT?: string; // Max high-confidence observations to retrieve (default: 50)
KNOWLEDGE_OBSERVATION_MAX_LENGTH?: string; // Max observation text length (default: 1000)
KNOWLEDGE_ENTITY_NAME_MAX_LENGTH?: string; // Max entity name length (default: 200)
KNOWLEDGE_DESCRIPTION_MAX_LENGTH?: string; // Max entity description length (default: 2000)
Expand Down Expand Up @@ -361,10 +363,6 @@ export interface Env {
GA4_FETCH_TIMEOUT_MS?: string; // Timeout for GA4 API fetch (default: 30000)
// File proxy configuration (chat file browser)
FILE_PROXY_TIMEOUT_MS?: string; // Timeout for VM agent file proxy requests (default: 15000)
BROWSER_PROXY_TIMEOUT_MS?: string; // Timeout for browser sidecar proxy requests (default: 30000)
// Neko browser sidecar cloud-init configuration
NEKO_IMAGE?: string; // Docker image for Neko browser sidecar (default: ghcr.io/m1k1o/neko/google-chrome:latest)
NEKO_PRE_PULL?: string; // Pre-pull Neko image during cloud-init: "true" or "false" (default: "true")
FILE_PROXY_MAX_RESPONSE_BYTES?: string; // Max response body size from VM agent file proxy (default: 2097152 = 2MB)
FILE_RAW_PROXY_MAX_BYTES?: string; // Max response size for raw binary file proxy (default: 52428800 = 50MB)
// File upload/download configuration
Expand Down Expand Up @@ -423,7 +421,7 @@ export interface Env {
TRIGGER_EXECUTION_LOG_RETENTION_DAYS?: string; // Days to retain completed/failed/skipped execution logs (default: 90)
TRIGGER_EXECUTION_CLEANUP_ENABLED?: string; // Kill switch: "false" to disable cleanup sweep (default: enabled)
TRIGGER_STALE_RECOVERY_BATCH_SIZE?: string; // Max stale executions to recover per sweep (default: 100)
// AI Inference Proxy (Workers AI gateway for trial users)
// AI Inference Proxy (Cloudflare AI Gateway for trial users)
AI_PROXY_ENABLED?: string; // Kill switch: "false" to disable (default: enabled)
AI_PROXY_DEFAULT_MODEL?: string; // Default Workers AI model (default: @cf/meta/llama-4-scout-17b-16e-instruct)
AI_PROXY_ALLOWED_MODELS?: string; // Comma-separated allowed models
Expand All @@ -433,4 +431,5 @@ export interface Env {
AI_PROXY_RATE_LIMIT_RPM?: string; // Requests per minute per user (default: 30)
AI_PROXY_STREAM_TIMEOUT_MS?: string; // Max streaming duration in ms (default: 120000)
AI_PROXY_RATE_LIMIT_WINDOW_SECONDS?: string; // Rate limit window in seconds (default: 60)
AI_GATEWAY_ID?: string; // Cloudflare AI Gateway ID (default: sam)
}
24 changes: 3 additions & 21 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { googleAuthRoutes } from './routes/google-auth';
import { knowledgeRoutes } from './routes/knowledge';
import { libraryRoutes } from './routes/library';
import { mcpRoutes } from './routes/mcp';
import { nodeLifecycleRoutes } from './routes/node-lifecycle';
import { nodesRoutes } from './routes/nodes';
import { notificationRoutes } from './routes/notifications';
import { deploymentIdentityTokenRoute,gcpDeployCallbackRoute, projectDeploymentRoutes } from './routes/project-deployment';
Expand Down Expand Up @@ -155,7 +156,7 @@ app.use('*', async (c, next) => {
log.info('ws_proxy_invalid_subdomain', { hostname, reason: parsed.error });
return c.json({ error: 'INVALID_WORKSPACE', message: 'Invalid workspace subdomain' }, 400);
}
const { workspaceId, targetPort, sidecar } = parsed;
const { workspaceId, targetPort } = parsed;

// Look up workspace routing metadata from D1.
const db = drizzle(c.env.DATABASE, { schema });
Expand Down Expand Up @@ -192,7 +193,6 @@ app.use('*', async (c, next) => {
nodeId: workspace.nodeId || workspaceId,
backendHostname,
targetPort,
sidecar,
method: c.req.raw.method,
path: url.pathname,
});
Expand All @@ -208,25 +208,6 @@ app.use('*', async (c, next) => {
vmUrl.hostname = backendHostname;
vmUrl.port = vmAgentPort;

// Route sidecar alias requests to the VM agent's sidecar proxy endpoint.
// ws-{id}--browser.example.com/foo → {backend}/workspaces/{id}/browser/proxy/foo
if (sidecar !== null) {
const subPath = url.pathname === '/' ? '' : url.pathname;
vmUrl.pathname = `/workspaces/${workspaceId}/${sidecar}/proxy${subPath}`;

try {
const { token } = await signTerminalToken('port-proxy', workspaceId, c.env);
vmUrl.searchParams.set('token', token);
} catch (err) {
log.error('sidecar_proxy_token_error', {
workspaceId,
sidecar,
...serializeError(err),
});
return c.json({ error: 'TOKEN_ERROR', message: 'Failed to generate sidecar proxy token' }, 500);
}
}

// Route port-specific requests to the VM agent's port proxy endpoint.
// ws-{id}--3000.example.com/foo → {backend}/workspaces/{id}/ports/3000/foo
if (targetPort !== null) {
Expand Down Expand Up @@ -371,6 +352,7 @@ app.route('/api/credentials', credentialsRoutes);
app.route('/api/providers', providersRoutes);
app.route('/api/github', githubRoutes);
app.route('/api/nodes', nodesRoutes);
app.route('/api/nodes', nodeLifecycleRoutes);
app.route('/api/workspaces', workspacesRoutes);
app.route('/api/terminal', terminalRoutes);
app.route('/api/agent', agentRoutes);
Expand Down
21 changes: 6 additions & 15 deletions apps/api/src/lib/workspace-subdomain.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import type { SidecarAlias } from '@simple-agent-manager/shared';
import { isSidecarAlias, SIDECAR_ALIASES } from '@simple-agent-manager/shared';

/**
* Parsed workspace subdomain result.
* Pattern: ws-{id}.{domain}, ws-{id}--{port}.{domain}, or ws-{id}--{sidecar}.{domain}
* Pattern: ws-{id}.{domain} or ws-{id}--{port}.{domain}
*/
export interface WorkspaceSubdomain {
workspaceId: string;
targetPort: number | null;
/** Named sidecar alias (e.g., 'browser') for routing to sidecar containers. */
sidecar: SidecarAlias | null;
}

/**
* Parse a workspace subdomain into workspace ID and optional port or sidecar alias.
* Parse a workspace subdomain into workspace ID and optional port.
*
* @param hostname - Full hostname (e.g., "ws-abc123--3000.example.com" or "ws-abc123--browser.example.com")
* @param hostname - Full hostname (e.g., "ws-abc123--3000.example.com")
* @param baseDomain - Base domain (e.g., "example.com")
* @returns Parsed result, or null if the hostname is not a workspace subdomain
*/
Expand All @@ -30,25 +25,21 @@ export function parseWorkspaceSubdomain(
const subdomain = hostname.replace(`.${baseDomain}`, '');
let workspaceId: string;
let targetPort: number | null = null;
let sidecar: SidecarAlias | null = null;

if (subdomain.includes('--')) {
const parts = subdomain.split('--', 2);
const wsSubdomain = parts[0] ?? '';
const suffix = (parts[1] ?? '').toLowerCase();
workspaceId = wsSubdomain.replace(/^ws-/, '').toUpperCase();

// Check if suffix is a known sidecar alias (e.g., 'browser')
if (isSidecarAlias(suffix)) {
sidecar = suffix;
} else if (/^\d+$/.test(suffix)) {
if (/^\d+$/.test(suffix)) {
const parsed = parseInt(suffix, 10);
if (parsed <= 0 || parsed > 65535) {
return { error: 'Port must be between 1 and 65535' };
}
targetPort = parsed;
} else {
return { error: `Unknown sidecar alias. Valid aliases: ${SIDECAR_ALIASES.join(', ')}` };
return { error: `Unknown subdomain suffix: ${suffix}` };
}
} else {
workspaceId = subdomain.replace(/^ws-/, '').toUpperCase();
Expand All @@ -63,5 +54,5 @@ export function parseWorkspaceSubdomain(
return { error: 'Invalid workspace ID format' };
}

return { workspaceId, targetPort, sidecar };
return { workspaceId, targetPort };
}
Loading
Loading