Skip to content

0.1.9

Choose a tag to compare

@ljagiello ljagiello released this 19 May 05:43
· 53 commits to main since this release
85c1dd9

Wire any external service to a workspace via webhooks. The tunnel no longer ships hardcoded bitbucket / jira / github providers — instead it hands the raw bytes to your workspace agent, which parses the body and verifies the signature itself. A new wiring-external-webhooks skill walks through the setup end-to-end (#354). OpenRouter joins as an opt-in LLM provider — set OPENROUTER_API_KEY and pick any tool-capable model from their catalog (#348). Installed Friday Studio stops hitting dead ports: workspace imports from the discover catalog and bundled-agent runs both work against the real daemon now (#386), and imported workspaces that reference bundled type: user agents actually run them instead of failing with "agent not found" (#349). Read Breaking changes before upgrading — webhook URLs have moved and the signal-trigger SDK response shape narrows to a discriminated union.

Breaking changes

  • Webhook-tunnel collapsed to a single raw provider — github, bitbucket, jira are all gone from the binary. tools/webhook-tunnel/provider/provider.go is now ~25 lines exposing Names() []string { return []string{"raw"} } / IsValid(name string) bool { return name == "raw" }; a new TestIsValidRejectsRemovedProviders in handler_test.go explicitly asserts the three dropped names are rejected. Deleted alongside: the embedded provider/mappings.yml allowlist (-76), provider/extract.go (extractByPath, splitArraySegment, -63) + extract_test.go (-92), provider/handler.go (Verify / Transform / event-filter pipeline, -143), and the entire tools/webhook-tunnel/passphrase/ package (passphrase.go -32, wordlist.go -46, passphrase_test.go -34). The Handler interface, Get(name), List(), Init(), EventMapping, ProviderConfig, and the gopkg.in/yaml.v3 dependency are out of provider/provider.go entirely. Workspaces previously registered on /hook/bitbucket/<workspaceId>/<signalId>, /hook/jira/..., or /hook/github/... no longer resolve — webhooks must move to /hook/raw/<workspaceId>/<signalId> and the workspace agent parses + (optionally) HMAC-verifies the body against the bytes Friday now hands over byte-for-byte. The /status JSON response loses its secret field; pattern is now "/hook/raw/{workspaceId}/{signalId}". The new wiring-external-webhooks skill is the canonical setup guide. (#354)
  • Webhook-tunnel WEBHOOK_SECRET + WEBHOOK_MAPPINGS_PATH env overrides + Config.WebhookSecret struct field removed. No replacement at the tunnel layer — signature material rides through the proxied request headers (the tunnel deliberately keeps authorization / cookie because Stripe / Slack / etc. carry signature bytes there) and verification happens inside the workspace agent. Per-workspace secret storage belongs in the workspace's own .env overlay (the supply side shipped in 0.1.8). The passphrase.Generate() fallback that previously synthesized a WEBHOOK_SECRET is gone with the package. (#354)
  • Tunnel forwarder body cap lowered from 25 MiB → 4 MiB. tools/webhook-tunnel/forwarder/forwarder.go switches from a Forward(...) helper that read+marshalled bodies into a buffer to two httputil.ReverseProxy returners — WebhookProxyHandler() (rewrites /hook/raw/... → /api/workspaces/.../signals/...) and ProxyHandler() (rewrites /platform/<provider>[/<suffix>] → /signals/<provider>[/<suffix>], preserving query strings for Meta WhatsApp's hub.verify_token / hub.challenge handshake). Bytes are base64-encoded into the JetStream envelope on publish; the 4 MiB ceiling keeps the post-base64 message under the JetStream max_msg_size headroom (DEFAULT_MAX_MSG_SIZE in signal-stream.ts raised to 8 MiB in this release). A Content-Length > maxBodySize pre-stream 413 short-circuit lands in handleHook so oversized uploads fail fast instead of partially streaming. Hop-by-hop header stripping (RFC 7230 §6.1) now comes for free via the stdlib reverse proxy. (#354)
  • WEBHOOK_TUNNEL_URL env override removed. getWebhookTunnelUrl() in apps/atlasd/src/services/communicator-wiring.ts now reads EXTERNAL_TUNNEL_URL only — the prior literal process.env.WEBHOOK_TUNNEL_URL is replaced. On installed Friday the launcher (tools/friday-launcher/cert_env.go) remaps webhook-tunnel to FRIDAY_PORT_WEBHOOK_TUNNEL (e.g. :19090) and exports EXTERNAL_TUNNEL_URL — but never WEBHOOK_TUNNEL_URL, so the prior variable silently fell through to a dead :9090. New "uses EXTERNAL_TUNNEL_URL when the launcher exports it (installed rig)" test asserts the bare 9090 default is never hit. (#390)
  • WorkspaceRuntime.triggerSignalWithSession / triggerSignalWithResult signatures changed. Five trailing positional optionals (streamId, onStreamEvent, skipStates, abortSignal, parentSessionId) collapsed into a single opts: TriggerSignalOpts = {} exported from @atlas/workspace. The bag picks up a new webhookContext: { body?: string; headers?: Record<string, string> } field threaded into engine.signal({ type, data, body, headers }). The matching daemon-layer wrapper triggerWorkspaceSignal in apps/atlasd/src/atlas-daemon.ts (+65) takes the same shape. Out-of-tree daemon embedders calling these methods must migrate from positional → options-bag; internal call sites are updated in the same PR. (#354)
  • AtlasClient.triggerSignal return type narrows to a discriminated union; old SignalTriggerResponseSchema in @atlas/client/schemas deleted. Returns either { status: "completed", sessionId, output?, summary? } (sync default) or { status: "accepted", correlationId } (async / ?nowait=true / webhook mode). Callers must narrow on status before reading sessionId / correlationId. The canonical schema moved to a new packages/core/src/signal-trigger-response.ts (+32) and is re-exported from @atlas/core (preferred) and @atlas/client (back-compat one-line import path). On response drift the client now throws AtlasApiError(..., 502) instead of returning a malformed envelope; the previous non-discriminated { message, status, workspaceId, signalId } shape is gone. (#354)

Added

  • Webhook fast path + ?nowait=true async mode on POST /api/workspaces/<id>/signals/<sig>. apps/atlasd/routes/workspaces/index.ts (+345) reads raw bytes via c.req.arrayBuffer(), JSON-parses defensively, and runs a tight envelope discriminator — the request is webhook mode iff the body is non-empty AND has at least one key AND every key is in SIGNAL_ENVELOPE_KEYS (derived from signalBodySchema.shape at module load with an assertion that the set is non-empty, so a zod-upgrade can't silently collapse the discriminator and route every envelope request to webhook mode). Mixed-shape bodies like {"payload": {...}, "action": "opened"} route to webhook mode so zValidator can't strip upstream fields. Hop-by-hop headers (RFC 7230 §6.1 + host / content-length) are stripped before publish; authorization / cookie are deliberately kept because Stripe / Slack / etc. carry signature material there. The body is base64-encoded into the JetStream envelope as webhookBody + webhookHeaders (new optional fields on SignalEnvelopeSchema); route returns 202 { status: "accepted", correlationId }. The same handler honors ?nowait=true for non-webhook callers: publishes to SIGNALS and returns 202 immediately without subscribing to the response stream. Accept: text/event-stream still wins over ?nowait (an index.test.ts test locks the priority). (#354)
  • envelopeToWebhookContext(envelope) + raised DEFAULT_MAX_MSG_SIZE in apps/atlasd/src/signal-stream.ts (+55, plus a new 360-line signal-stream.test.ts). New pure helper returns { body, headers } | undefined — explicitly undefined rather than {} when neither field is set, so non-webhook signals don't pollute ctx.input.raw["body"] with "". SignalEnvelopeSchema + PublishSignalOpts gain optional webhookBody (base64) and webhookHeaders (lowercased Record<string, string>). DEFAULT_MAX_MSG_SIZE raised from 1 MiB → 8 MiB to match FRIDAY_JETSTREAM_MAX_MSG_SIZE in packages/jetstream/src/config.ts — the old 1 MiB applied only to test-constructed streams and would have let a base64 webhook body pass tests but 500 against a real daemon. Tests round-trip webhook fields through publish → consume → schema and unit-cover all four envelopeToWebhookContext shapes. (#354)
  • SignalTriggerResponseSchema shipped from @atlas/core as the canonical signal-trigger response shape. New packages/core/src/signal-trigger-response.ts (+32) + a packages/core/src/signal-trigger-response.ts export from packages/core/mod.ts. Discriminated union over status — "completed" carries sessionId + optional output + optional summary (sync); "accepted" carries correlationId (async / ?nowait / webhook). Producer (atlasd routes) and every consumer (mcp-server signals/trigger.ts, atlas-cli signals/trigger.ts, workspace-chat job-tools.ts, run-job-dialog.svelte, @atlas/client SDK) zod-parse against the same source. (#354)
  • FSM engine carries webhook body / headers through inputFrom action chaining. Signal and Context.input in packages/fsm-engine/types.ts (+31) gain optional body?: string / headers?: Record<string, string> siblings of data / config; PrepareResultSchema adds the same. parsePrepareResult no longer returns undefined when only body/headers are set (previously required task or config, silently dropping __lastPrepare carryover for webhook signals whose downstream code action returned no structural data). Two seams fixed in fsm-engine.ts (+44): initial prepareResult from a signal propagates sig.body / sig.headers to the top level (not nested under config); the inputFrom merge at the second-action-in-a-job path explicitly preserves body / headers from upstream — previously the inputSnapshot spread overwrote them with undefined, so a two-state validate → process job saw the body in action #1 but not action #2. New ~100-line case in tests/prepare-result.test.ts drives a two-state FSM and asserts both actions see the body. (#354)
  • OpenRouter as a sixth opt-in LLM provider. New packages/llm/src/openrouter.ts (+38) wraps @ai-sdk/openai against https://openrouter.ai/api/v1 via createChatCompletionsProvider (same rationale as LiteLLM — the Responses API breaks tool-use for non-OpenAI models). Set OPENROUTER_API_KEY and write conversational: openrouter:anthropic/claude-sonnet-4.6 (or any tool-capable slug) in friday.yml. OPENROUTER_PROXY_URL is honored via createProxyFetch. The catalog hits https://openrouter.ai/api/v1/models?supported_parameters=tools so the picker isn't drowned by OpenRouter's 500+ slugs, and sends HTTP-Referer: https://hellofriday.ai/ + X-OpenRouter-Title: Friday attribution headers so usage shows up on openrouter.ai/rankings. REGISTRY_PROVIDERS / RegistryModelId extended; PROVIDER_ENV_VARS.openrouter = "OPENROUTER_API_KEY"; PROVIDER_META.openrouter carries letter R, prefix sk-or-v1-, help URL openrouter.ai/keys. Default platform models untouched — pure opt-in. (#348)
  • @friday/wiring-external-webhooks system skill. New 158-line packages/system/skills/wiring-external-webhooks/SKILL.md covering: URL shape (https://<public-tunnel-url>/hook/raw/<workspaceId>/<signalId> — explicit warning not to give upstreams the direct /api/workspaces/.../signals/... route, which isn't exposed via cloudflared); discovery via curl -sk "${EXTERNAL_TUNNEL_URL:-https://localhost:9090}/status" | jq (empty body → wrong port for this rig; ps auxww | grep cloudflared to read the actual --url); the ctx.input.config or ctx.input.raw landing site; the silent-strip foot-gun where a nested type: object without additionalProperties: true turns the body into {} for the agent; the tunnel error catalog (400 Unknown provider, 400 raw provider expects JSON object, 400 empty body, 413 body exceeds N bytes, 502 Cannot reach atlasd); and a concurrency: skip first-action guard. Cross-referenced from writing-workspace-signals (+11) and exercised end-to-end by a new tools/qa/live-daemon/scenarios/webhook-setup-bitbucket.ts (+1867) plus a matching promptfoo trio (webhook-setup-bitbucket-{config,provider,tests} +44/+103/+67) covering 8 contract checks. (#354)
  • Tier 0 "Reading Signal Payloads" section in writing-friday-python-agents/references/examples.md (+81). Anti-pattern: parse_input(prompt) scrapes JSON from the enriched prompt string, which holds the action config ({config: {prompt, outputTo, ...}}), not the webhook body — wrong dict for webhook-direct invocations. Correct pattern: payload = ctx.input.config or ctx.input.raw. Includes a full wired workspace.yml example pinning initial: idle (not initial: process) — the "Missing sessionId in signal context" error users hit was caused by parking the agent in the initial-state entry actions, because signal context isn't bound to initial-state entries yet. (#354)

Changed

  • Link GitHub provider accepts five new token prefixes. GitHubSecretSchema.access_token .refine() in apps/link/src/providers/github.ts expanded from ghp_ / github_pat_ to also accept gho_ (user-to-server), ghu_ (user-to-user), ghs_ (server-to-server), ghr_ (refresh). Error message rewritten to enumerate all six prefixes. Enables saving tokens minted by gh auth / GitHub Apps, not just classic PATs. (#354)
  • friday-cli skill rewritten to document the three signal-trigger response modes. packages/system/skills/friday-cli/SKILL.md (+60) splits the previously-single example into explicitly labeled modes of /api/workspaces/<id>/signals/<sig>: (a) ?nowait=true → 202 {status:"accepted", correlationId, ...} returning in <100 ms (the mode webhook-tunnel uses internally); (b) sync JSON default → 200 {status:"completed", sessionId, output, summary} (connection held up to 10 min, with a warning about upstream 30 s caps); (c) SSE via Accept: text/event-stream (subscribe-then-publish must happen in one handler — cascade response is core-NATS with no JetStream replay, so a separate GET /signals/stream/<correlationId> would race fast cascades). references/cli.md agent exec no longer hardcodes --url http://localhost:5200 — installed Friday Studio uses $FRIDAY_PORT_PLAYGROUND (e.g. :15200), in-tree dev uses :5200. (#354, #390)
  • workspace-chat job-tools guard against an accepted response. Job tools always run sync (never pass ?nowait=true), but the runtime destructure const { sessionId, status, output, artifactIds, summary } = result.data would silently produce undefined sessionId/output if the route default ever flipped to async. packages/system/agents/workspace-chat/tools/job-tools.ts (+24) now narrows on data.status === "accepted" before destructuring, logs "Job tool received nowait (accepted) response — expected synchronous completion", and returns { success: false, status: "accepted", error: "Job '<name>' returned status: accepted. Job tools require the synchronous trigger response (do not pass ?nowait=true)." }. Not breaking — guard surfaces a clear chat-side error if a future route default ever flips, instead of "no sessionId, no output". (#354)
  • mcp-server signals/trigger goes through a typed mapper. New pure mapSignalTriggerResponse(workspaceId, signalId, response) in packages/mcp-server/src/tools/signals/trigger.ts (+80) replaces an as-cast with a runtime SignalTriggerResponseSchema.safeParse; drift surfaces as createErrorResponse(...) to the LLM instead of as any-laundered garbage. New 96-line trigger.test.ts covers both union branches + drift rejection (queued/failed/missing-field cases). The atlas-cli signals/trigger.ts (+15) gets the matching status === "completed" discriminator branch, defensive against future shape drift since the CLI never passes ?nowait. (#354)
  • Dependabot auto-sync workflow rebuilt to keep every lockfile and deno.json pin in lock-step with package.json. Three follow-on commits compose: (1) new scripts/sync-deno-json-npm-versions.ts (+129) walks every package.json (deps / devDeps / peerDeps), keeps the highest semver per name, and rewrites root + workspace deno.json only when parseRange(existing) can't satisfies(parseSemver(target)) — caret pins that already cover the bump produce no churn; the existing ^ / ~ / exact prefix is preserved via rangePrefix(). Wired into .github/workflows/dependabot-lockfile.yml as Sync deno.json npm versions + Re-install after deno.json sync; workflow renamed to Sync Deno lockfile + deno.json for Dependabot PRs. Fixed dual-package-hazard typecheck failures (Property '[GET_MATCH_RESULT]' missing) that hit every open dependabot PR when deno.json pinned hono@4.12.18 but package.json was bumped to 4.12.19; same drift had stranded @hono/zod-validator (deno.json ^0.7.5 vs package.json ^0.8.0 — ^ on 0.x doesn't cross minor). (fd42bcec1) (2) Adds root deno.json deno.jsonc to the git-auto-commit-action file_pattern — **/deno.json globstar doesn't reliably match the root file. (7ab3f0f26) (3) Adds a setup-node@v6 + npm install --package-lock-only --prefix apps/studio-installer step so the standalone installer lockfile (isolated from the npm workspace) gets regenerated too — without it, #381 and #385 had left studio-installer-build.yml failing npm ci across all three OS jobs with lock file's svelte@5.55.7 does not satisfy svelte@5.55.8. (#388)
  • @anthropic-ai/claude-agent-sdk rolled 0.2.141 → 0.3.143. Labelled semver-minor by Dependabot but spans a 0.x major-equivalent jump. Transitively bumps @anthropic-ai/sdk 0.81.0 → 0.96.0 and pins @modelcontextprotocol/sdk to 1.28.0 (down from 1.29.0). Touches packages/bundled-agents and packages/llm. Pre-1.0 SDKs typically carry API-surface changes between minors; if you've forked or extended the bundled-agent surface, smoke-test before upgrading. (#384)
  • @hono/mcp 0.2.5 → 0.3.0 across apps/atlasd and apps/link. Pre-1.0 minor bump on the MCP server-wiring surface; also pulls hono-rate-limiter in transitively. (#374)
  • chat + adapters (@chat-adapter/slack / discord / teams / telegram / whatsapp) 4.28.1 → 4.29.0 across apps/atlasd. Minor bump but broad runtime surface — worth a smoke test on any communicator wiring. (#375)

Fixed

  • Tunnel forwarder switched to httputil.ReverseProxy for byte-faithful pass-through. tools/webhook-tunnel/forwarder/forwarder.go swaps a Forward(...) helper that read-and-marshalled bodies for two stdlib reverse-proxy returners — WebhookProxyHandler() and ProxyHandler(). Method / body / headers / query string preserved; only host + path rewritten. The prior buffer-and-remarshal path made signature verification against r.Body bytes impossible (header / body whitespace was lost on the rewrite); reverse-proxy passes bytes through unmodified, which is what unlocks the workspace-side HMAC verification the new skill teaches. (#354)
  • Playground discover + execute routes resolve the daemon URL at runtime instead of build-time. tools/agent-playground/src/lib/server/routes/{discover,execute}.ts imported the build-time-frozen DAEMON_BASE_URL constant — vite froze it from __FRIDAY_DAEMON_BASE_URL__ at npm run build, and since the production build runs without FRIDAYD_URL set, the baked value was the fallback http://localhost:8080. The launcher then started the real daemon on https://localhost:18080, so every workspace import from the discover catalog and every bundled-agent run hit a dead port — UI surface was "Import failed (500)". New effectiveDaemonUrl() / effectiveTunnelUrl() in src/lib/server/daemon-url.ts read vite's define in dev and fall through to runtime FRIDAYD_URL / EXTERNAL_TUNNEL_URL in the compiled binary; the trap constants DAEMON_BASE_URL and TUNNEL_BASE_URL are deleted. Same fix threads into static-server.ts (inline EXTERNAL_TUNNEL_URL read replaced with helper) and the dev catch-all proxies /api/daemon/[...path] + /api/tunnel/[...path]. (#386)
  • Workspace bundle imports install bundled type: user agents into the global agents dir. POST /import-bundle and POST /import-bundle-all had extracted bundled user agents to <targetDir>/agents/<name>/ (workspace-local) but never copied them to <atlasHome>/agents/<id>@<version>/ — the only path UserAdapter scans — and never called registry.reload(), so imported workspaces that referenced type: user agents failed at runtime with "agent not found." New installImportedAgents in apps/atlasd/routes/workspaces/bundle-helpers.ts (+182) reads metadata.json, atomic tmp+rename copies into the global dir, calls registry.reload() after either endpoint installs anything new; import responses gain agentsInstalled / agentsSkipped counts. Three hardenings ride along: (1) isPathSafeIdent rejects /, \, NUL, .., ., leading/trailing dots, >128 chars before joining — blocks a malicious bundle whose metadata.json.id = "../../etc/cron.d/x" from writing outside the agents root, per-agent skip not whole-import abort; (2) on <id>@<version> already-installed collisions, hashPrimitive compares content and emits a WARN when divergent (previously silent: workspace B with the same id@version but different code would run workspace A's bytes); (3) reuses AgentMetadataFileSchema (now re-exported from @atlas/core/agent-loader/index.ts) instead of ad-hoc typeof checks. Closes the export-side gap from resolveExternalUserAgents (#288). (#349)
  • @ai-sdk/mcp pinned to 1.0.41 — 1.0.42 broke every HTTP MCP connection. @ai-sdk/mcp@1.0.42 assigns this.transport.protocolVersion = result.protocolVersion, but StreamableHTTPClientTransport exposes protocolVersion as a getter only — so every HTTP MCP server connection threw "Cannot set property protocolVersion of #<StreamableHTTPClientTransport> which has only a getter" and the server was skipped. packages/mcp/package.json pinned to 1.0.41; .github/dependabot.yml gains an ignore block for @ai-sdk/mcp >= 1.0.42 plus a new @tanstack/* group so query-core and svelte-query move together. Secondary fallout patched: PR #364 had bumped @tanstack/query-core to 5.100.10 while svelte-query@6.1.28 still bundled 5.100.9, collapsing two QueryFunction type identities and producing ~40 createQuery inference errors across agent-playground — fixed by bumping @tanstack/svelte-query to 6.1.29. createMCPTools also logs err.cause on swallowed connection errors so wrapped subprocess stderr is visible without patching. (#370)
  • MCP tool invoker and usage panels render the resolved workspace display name. Both mcp-tool-invoker.svelte and mcp-workspace-usage.svelte called workspaceQueries.list() and bound to the raw daemon name field. For templated workspaces the default name is the literal string workspace, so the dropdown surfaced two indistinguishable workspace entries — users couldn't tell which workspace they were invoking a tool against. Switched to workspaceQueries.enriched() + ws.displayName, matching the sidebar / command palette / other pickers. (#353)

Deps

  • Routine patch / minor bumps batched by Dependabot: ai-sdk group across two rounds (ai 6.0.177→6.0.184, @ai-sdk/anthropic 3.0.76→3.0.78, @ai-sdk/google 3.0.71→3.0.75, @ai-sdk/openai 3.0.63→3.0.64, @ai-sdk/svelte 4.0.177→4.0.184) (#357, #377); @vitest/coverage-v8 + vitest patch bumps (#358, #367); @tanstack/svelte-table alpha.41 → alpha.46 → alpha.48 over two PRs (#361, #378); @sveltejs/kit 2.59.0 → 2.60.1 (#363); svelte 5.55.7 → 5.55.8 (#381); @tanstack/query-core 5.100.8 → 5.100.10 (#364); react 19.2.5 → 19.2.6 (#362); xstate 5.31.0 → 5.31.1 (#359); vite 7.3.2 → 7.3.3 (#385); prettier-plugin-svelte 3.5.1 → 3.5.2 (#383); svelte-eslint-parser 1.6.0 → 1.6.1 (#365); typescript-eslint 8.59.2 → 8.59.3 (#366); knip 6.11.0 → 6.14.0 (#360); dompurify 3.4.2 → 3.4.5 (#379); @libpdf/core 0.3.4 → 0.3.6 (#380); yaml 2.8.4 → 2.9.0 (#382); @hono/zod-validator 0.7.6 → 0.8.0 (#356); hono 4.12.18 → 4.12.19 (#374); @anthropic-ai/claude-code 2.1.137 → 2.1.143 in /docker (#355); Tauri group in apps/studio-installer/src-tauri (#373); github.com/aymanbagabas/go-pty 0.2.2 → 0.2.3 (#372).