Skip to content

feat: support automations without repositories#836

Open
rubenlangeweg wants to merge 13 commits into
ColeMurray:mainfrom
rubenlangeweg:feat/no-repository-automations
Open

feat: support automations without repositories#836
rubenlangeweg wants to merge 13 commits into
ColeMurray:mainfrom
rubenlangeweg:feat/no-repository-automations

Conversation

@rubenlangeweg

@rubenlangeweg rubenlangeweg commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Automations can now run without a repository target, so operational workflows can start agent sessions without cloning a code workspace or inventing a placeholder repo. Repository-backed automations still resolve through SCM access before session creation, while no-repository sessions disable repo-only behavior like code-server, child sessions, and PR creation.

Current boundary: no_repository automations are coordinator sessions only; code investigation or PR work should start a separate repo-bound session after a repository is discovered.

Why

This is the first target-resolution slice for #833. It enables operational automations that do not know a repository at trigger time, such as an incident/catalog workflow that first investigates external context, then starts a separate repo-bound session only after the relevant repository is discovered.

What Changed

  • Adds fixed_single_repo / no_repository target handling at automation creation and scheduling time.
  • Allows automation and session rows to store null repository fields through D1 migration 0029_allow_no_repository_targets.sql.
  • Teaches sandbox startup to skip clone/setup hooks when no repository is present while preserving the existing repository path for normal sessions.
  • Updates web session and automation UI labels so no-repository sessions render as a first-class state.

Below is a recording of the working implementation deployed on CF.

CleanShot.2026-06-27.at.12.44.34.mp4

Validation

  • npm run typecheck
  • npm run lint
  • npm test
  • packages/control-plane: analytics integration test via vitest.integration.config.ts
  • packages/modal-infra: focused pytest and ruff on touched files
  • packages/sandbox-runtime: focused pytest on touched runtime tests
  • D1 migration validated against a seeded pre-0029 SQLite database with PRAGMA foreign_key_check = 0
  • git diff --check

Refs #833


Compound Engineering
GPT-5

Made with Orca 🐋

Summary by CodeRabbit

  • New Features
    • Added automation/session “no repository” support with a target mode selector (fixed_single_repo vs no_repository), including repository configuration UI and consistent “No repository” labeling.
  • Bug Fixes
    • Improved handling of missing/nullable repo context across automation resolution, session creation, PRs, SCM credentials, sandbox feature gating/tool installs, analytics repo breakdowns, and scheduler/Slack audit + callback payloads.
    • Added clearer validation and compatibility errors for unsupported or incompatible target modes.
  • Documentation
    • Updated automation setup documentation with revised repository configuration flow, required fields, instruction limits, and supported automation target modes.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds support for automations and sessions without repository targets. It updates shared types, schema, route validation, sandbox/runtime wiring, analytics, and UI labels to carry targetMode and nullable repository fields through the system.

Changes

No-repository target support

Layer / File(s) Summary
Target-mode and nullable model
docs/AUTOMATIONS.md, packages/shared/src/{triggers/testing.ts,types/index.ts}, packages/control-plane/src/db/{automation-store.ts,automation-store.test.ts,session-index.ts,session-index.test.ts}, packages/control-plane/src/session/{repository.ts,schema.ts,schema.test.ts,types.ts}, terraform/d1/migrations/0029_allow_no_repository_targets.sql, packages/control-plane/src/db/{analytics-store.ts,mcp-servers.ts}, packages/control-plane/src/sandbox/{client.ts,provider.ts,sandbox-env.ts}, packages/control-plane/src/sandbox/providers/{daytona-provider.ts,vercel/provider.ts}
targetMode and nullable repo fields are added to the shared and persisted data model, the schema migration/test coverage is updated to match, analytics now emits a No repository bucket, and sandbox request/config/provider contracts accept null repo identifiers.
Automation resolution and scheduling
packages/control-plane/src/automation/{target-resolution.ts,target-resolution.test.ts}, packages/control-plane/src/routes/automations.{ts,test.ts}, packages/control-plane/src/scheduler/{durable-object.ts,durable-object.test.ts}, packages/control-plane/test/integration/{scheduler-slack-events.test.ts,webhooks-slack.test.ts}
targetMode is validated and resolved for automation creation, and scheduler/session setup now uses the resolved target data and No repository callback labels.
Session guards and repo-less flow
packages/control-plane/src/session/{create-session-input.ts,durable-object.ts,initialize.ts,integration-settings-resolution.ts,openai-token-refresh-service.ts,pull-request-service.ts,repository.ts,types.ts}, packages/control-plane/src/session/http/handlers/{child-sessions.handler.ts,pull-request.handler.ts,sandbox.handler.ts,sandbox.handler.test.ts,session-lifecycle.handler.ts,session-lifecycle.handler.test.ts}, packages/control-plane/src/routes/{session-create.ts,session-child-spawn.ts}, packages/control-plane/src/router.create-session.test.ts, packages/control-plane/src/router.spawn-child.test.ts
Session creation and session-scoped handlers now reject missing repository targets or return null repo fields, and token refresh/session init paths preserve nullable repo fields.
Control-plane sandbox contracts
packages/control-plane/src/db/mcp-servers.ts, packages/control-plane/src/sandbox/{client.ts,provider.ts,sandbox-env.ts,lifecycle/manager.ts,lifecycle/manager.test.ts,providers/{daytona-provider.ts,vercel/provider.ts}}
Sandbox request/config types, provider env vars, and lifecycle gating now distinguish repo-backed sessions from no-repository sessions.
Modal sandbox boot and runtime
packages/modal-infra/src/{sandbox/manager.py,web_api.py}, packages/modal-infra/tests/{test_sandbox_env_vars.py,test_web_api_create_sandbox.py}, packages/sandbox-runtime/src/sandbox_runtime/{entrypoint.py,types.py}, packages/sandbox-runtime/tests/{test_entrypoint_build_mode.py,test_tool_installation.py}
Modal sandbox creation/restore and sandbox runtime now use REPOSITORY_MODE="none" for missing repo context and skip clone/token wiring and repo-only hooks.
UI labels and notifications
packages/web/src/lib/repo-label.ts, packages/web/src/app/(app)/automations/[id]/*, packages/web/src/components/automations/*, packages/web/src/components/global-command-menu.tsx, packages/web/src/components/session-header.tsx, packages/web/src/components/session-sidebar.tsx, packages/web/src/components/settings/data-controls-settings.tsx, packages/web/src/components/sidebar/*, packages/web/src/components/ui/form-controls.tsx, packages/control-plane/src/routes/slack-notify.ts, packages/control-plane/src/routes/slack-notify.test.ts, packages/control-plane/test/integration/analytics.test.ts
A shared repo-label formatter is used across the web UI, analytics, and Slack notifications, and the automation form now exposes the no-repository target selector.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • open-inspect
  • ColeMurray

Poem

I hopped through targets, bright and new,
with No repository in view.
The sandbox hummed, the labels gleamed,
and branchless carrots softly streamed. 🐰
Hop hop — the flows all lined up true.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding support for automations that can run without repositories.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rubenlangeweg rubenlangeweg force-pushed the feat/no-repository-automations branch from 352cbb7 to 2c77210 Compare June 26, 2026 18:37
Co-authored-by: Orca <help@stably.ai>
@rubenlangeweg rubenlangeweg force-pushed the feat/no-repository-automations branch from 2c77210 to 21d3712 Compare June 26, 2026 18:39
Co-authored-by: Orca <help@stably.ai>
@rubenlangeweg rubenlangeweg marked this pull request as ready for review June 26, 2026 19:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/control-plane/src/session/http/handlers/pull-request.handler.ts (1)

56-74: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate PR creation on repo target before auth lookup, and don't require base_branch.

SessionPullRequestService already falls back to repoInfo.defaultBranch, so this new check turns repo-backed sessions with a null base_branch into false 400s. It should also run before participant/auth resolution so repo-less sessions consistently fail on the intended boundary instead of surfacing an unrelated auth error.

Suggested fix
       const session = deps.getSession();
       if (!session) {
         return Response.json({ error: "Session not found" }, { status: 404 });
       }
+      if (!session.repo_owner || !session.repo_name) {
+        return Response.json(
+          { error: "Pull requests require a repository target" },
+          { status: 400 }
+        );
+      }
 
       const promptingParticipantResult = await deps.getPromptingParticipantForPR();
       if (!promptingParticipantResult.participant) {
         return Response.json(
           { error: promptingParticipantResult.error },
@@
-      if (!session.repo_owner || !session.repo_name || !session.base_branch) {
-        return Response.json(
-          { error: "Pull requests require a repository target" },
-          { status: 400 }
-        );
-      }
-
       const result = await deps.createPullRequest({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/session/http/handlers/pull-request.handler.ts`
around lines 56 - 74, Move the repository-target validation in
pull-request.handler’s request flow so it runs before
getPromptingParticipantForPR/resolveAuthForPR, and change the guard to only
require repo_owner and repo_name. Remove base_branch from the required-session
check so SessionPullRequestService can continue falling back to
repoInfo.defaultBranch, and keep the existing 400 response for repo-less
sessions at that earlier boundary.
packages/control-plane/src/routes/slack-notify.ts (1)

88-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a no-repo/global denial message on this fallback path.

When repoScope is null, this branch is using global Slack settings, but the error still says notifications are disabled “for this repository”. That will mislead users debugging no-repository sessions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/slack-notify.ts` around lines 88 - 99, The
fallback path in slack-notify.ts uses global Slack settings when repoScope is
null, but the denial message still says the feature is disabled “for this
repository.” Update the failureResponse text in this branch to use a
no-repo/global wording, and keep the change aligned with the existing
resolveSlackSettings, logDenial, and failureResponse flow so users get an
accurate message for global sessions.
🧹 Nitpick comments (5)
packages/control-plane/src/sandbox/lifecycle/manager.test.ts (1)

503-513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the session-id sandboxId fallback.

This case covers the repo-null payload, but it doesn't verify the new buildSandboxIdForSession() fallback that switches repo-less sessions to session.id. Adding that expectation would lock down the core contract change in doSpawn().

Suggested test tweak
-      const storage = createMockStorage(
-        createMockSession({
+      const session = createMockSession({
           repo_owner: null,
           repo_name: null,
           repo_id: null,
           base_branch: null,
           code_server_enabled: 1,
-        }),
-        sandbox
-      );
+        });
+      const storage = createMockStorage(session, sandbox);
...
       expect(provider.createSandbox).toHaveBeenCalledWith(
         expect.objectContaining({
+          sandboxId: expect.stringContaining(`sandbox-${session.id}-`),
           repoOwner: null,
           repoName: null,
           branch: null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/sandbox/lifecycle/manager.test.ts` around lines
503 - 513, The repo-null spawn test in manager.test.ts should also assert the
new sandboxId fallback used by doSpawn(). Update the expectation around
provider.createSandbox to verify that buildSandboxIdForSession() resolves to
session.id when repoOwner, repoName, and branch are null, so the test locks down
the repo-less session contract change.
packages/control-plane/src/scheduler/durable-object.test.ts (1)

481-500: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that target-resolution failure aborts before session creation.

This test only checks the failed run record. If /internal/tick creates a session before marking the run failed, the test still passes and we leak orphan session state. Add a no-session-created assertion here as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/scheduler/durable-object.test.ts` around lines 481
- 500, The durable-object test for /internal/tick only verifies the failed run
record and misses the case where a session could still be created before
failure. Update the test around createSchedulerDO and scheduler.fetch to also
assert that no session is created when mockCheckRepositoryAccess returns null,
using the relevant session-creation mock or assertion in this spec so
target-resolution failure is verified to abort before any session state is
created.
packages/control-plane/src/routes/automations.test.ts (1)

214-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the no-repo create path never resolves SCM access.

This case only checks the persisted null repo fields. If handleCreateAutomation() regressed and still called resolveRepoOrError() before discarding the result, the test would still pass while reintroducing the dependency this feature is meant to remove. Add an explicit not.toHaveBeenCalled() assertion for the resolver in this scenario.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/automations.test.ts` around lines 214 -
246, The no_repository automation test only verifies persisted null repo fields
and could miss an accidental SCM lookup in handleCreateAutomation(). Update the
no_repository test case in automations.test.ts to also assert the repo resolver
path is not used by checking the resolver mock (the resolveRepoOrError-related
mock) was not called. Keep the existing create assertions, and add the negative
call assertion in the no_repository scenario so regressions that still resolve
SCM access are caught.
packages/shared/src/types/index.ts (1)

771-805: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Model target-specific automation shapes as a discriminated union.

Automation and CreateAutomationRequest now represent two mutually exclusive states, but these flat interfaces still allow impossible combinations like { targetMode: "no_repository", repoOwner: "acme" } or { targetMode: "fixed_single_repo" }. That pushes the new contract back into runtime checks and test helpers instead of letting TypeScript catch it.

As per coding guidelines, "When threading existing fields through new code paths in TypeScript, evaluate whether the existing design (naming, types, units) is correct and fix bad names or units in the same change rather than propagating problems."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/index.ts` around lines 771 - 805, Model Automation
and CreateAutomationRequest as discriminated unions keyed by targetMode instead
of flat interfaces, so TypeScript can enforce the two valid shapes. Update the
Automation and CreateAutomationRequest definitions in the shared types module to
split fixed_single_repo from no_repository, making
repoOwner/repoName/repoId/baseBranch required only where applicable and absent
or null where not. Keep targetMode as the discriminator and adjust any related
TriggerConfig typing or dependent fields so impossible combinations are no
longer representable.

Source: Coding guidelines

packages/control-plane/src/routes/slack-notify.test.ts (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset getGlobal between tests.

vi.clearAllMocks() only clears call history. After Line 428 sets a resolved value here, later cases keep that implementation if handleSlackNotify ever starts consulting getGlobal for repo-backed sessions, which can turn that regression into a false green.

Suggested fix
 beforeEach(() => {
   vi.clearAllMocks();
+  integrationStoreMock.getGlobal.mockReset();
   sessionFetchMock.mockResolvedValue(new Response("{}", { status: 200 }));
   vi.stubGlobal("fetch", fetchMock);

Also applies to: 420-430

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/slack-notify.test.ts` at line 13, Reset the
mocked getGlobal between tests so one case’s resolved value does not leak into
later cases. Update the slack-notify test setup around getGlobal and the
affected scenarios in handleSlackNotify to restore the mock implementation in
test teardown or reinitialize it per test, since vi.clearAllMocks only clears
calls and not return values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/control-plane/src/db/automation-store.ts`:
- Around line 69-80: The target mode mapping in automation-store is coercing
unknown values into "fixed_single_repo" instead of validating them. Update
automationTargetMode() and the toAutomation() path to reject unsupported
row.target_mode values with an error, matching resolveAutomationTarget(), so
invalid modes are not returned as valid automations; keep bindAutomationInsert()
unchanged and ensure any parsing/validation failure is surfaced from
toAutomation().

In `@packages/control-plane/src/db/session-index.ts`:
- Around line 105-106: Normalize repository fields before persistence in the
session index mapping so blank or whitespace-only values are stored as null
instead of empty strings. Update the logic around session.repoOwner and
session.repoName in the session-index write path to trim and convert blank
values to null, keeping the existing lowercase normalization for real values;
this ensures the downstream no-repo handling works consistently.

In `@packages/control-plane/src/routes/session-create.ts`:
- Around line 42-43: The guard in session-create should distinguish between a
true no-repo request and a partial repo payload. Update the validation around
the repository fields so `NO_REPOSITORY_SESSIONS_AUTOMATION_ONLY_ERROR` is
returned only when both `body.repoOwner` and `body.repoName` are missing, and
when just one is absent, fall through to the existing validation error handling
instead. Use the existing request handling in `session-create` to keep the
repo-backed payload checks consistent.

In `@packages/control-plane/src/sandbox/providers/daytona-provider.ts`:
- Around line 205-207: The Daytona sandbox label generation still emits
openinspect_repo even when config.repoOwner/config.repoName are absent, which
produces malformed repo metadata for repo-less sessions. Update buildLabels() in
the Daytona provider to omit openinspect_repo entirely or special-case it when
REPOSITORY_MODE is "none", and only set it when both repoOwner and repoName are
present so labels stay valid.

In `@packages/control-plane/src/sandbox/providers/vercel/provider.ts`:
- Around line 346-348: The Vercel provider tag construction in buildTags() still
assumes repoOwner and repoName are present, which creates malformed
openinspect_repo values like “null/null” for repo-less sessions. Update
buildTags() to mirror buildEnvVars() by checking REPOSITORY_MODE or the nullable
repo fields and emit a safe non-repo tag value when
config.repoOwner/config.repoName are missing, keeping metadata consistent with
the provider’s “none” mode.

In `@packages/control-plane/src/scheduler/durable-object.ts`:
- Around line 74-80: The Slack target label is currently derived from legacy
repo fields in formatAutomationTargetLabel, which can show stale owner/name
after a target_mode switch to "no_repository". Update the label logic to use
target_mode as the source of truth, matching resolveAutomationTarget, so
repo-less automations always render "No repository" even if repo_owner/repo_name
are still populated. Apply the same fix wherever this formatter is used in the
durable-object scheduler flow so notifications stay consistent with
AutomationRow target_mode.

In
`@packages/control-plane/src/session/http/handlers/session-lifecycle.handler.ts`:
- Around line 21-25: Reject partial repository targets during session
initialization: in session-lifecycle.handler (and the session init path that
persists repo metadata), require repoOwner and repoName to be provided together
or not at all, and do not allow repoId to be stored independently when the repo
target is incomplete. Update the validation/persistence logic so mixed states
are rejected before saving the session, keeping the session row invariant
consistent for downstream handlers.

In `@packages/control-plane/src/session/schema.ts`:
- Around line 389-445: The session table rebuild is not atomic, so a failure
midway can leave the database half-migrated and lose rows; wrap the rebuild
sequence in a transaction inside the migration callback used by
applyMigrations(). Keep the existing logic in the run(sql) migration block, but
execute the DROP/CREATE/INSERT/RENAME steps as one unit so any error rolls back
cleanly instead of leaving session_new or a dropped session table behind.

In `@packages/modal-infra/src/sandbox/manager.py`:
- Around line 63-64: The _repository_mode helper is treating partial repo
metadata as "none", which hides corrupted repository-backed state. Update
_repository_mode to only return "single" when both repo_owner and repo_name are
present, and otherwise reject partial tuples by surfacing an error or explicit
invalid state so create/restore cannot silently lose repository context.

In `@packages/web/src/components/session-header.tsx`:
- Around line 43-47: The session header fallback logic in session-header should
treat null repo fields as a loaded no-repository state instead of staying on
"Loading session...". Update the repoLabel computation around the sessionState /
resolvedRepoOwner / resolvedRepoName branching so it uses the presence of
fallback session data to decide between "No repository" and loading, and make
sure the title fallback reuses that corrected label from the same logic.

In `@packages/web/src/components/sidebar/metadata-section.tsx`:
- Around line 27-30: The MetadataSection component still skips the repository
row when repoOwner and repoName are null, so update its render path to show an
explicit “No repository” state instead of omitting it. Adjust the logic in
MetadataSection to handle repo-less sessions using the new nullable props and
keep the repository row visible with a clear fallback label.

---

Outside diff comments:
In `@packages/control-plane/src/routes/slack-notify.ts`:
- Around line 88-99: The fallback path in slack-notify.ts uses global Slack
settings when repoScope is null, but the denial message still says the feature
is disabled “for this repository.” Update the failureResponse text in this
branch to use a no-repo/global wording, and keep the change aligned with the
existing resolveSlackSettings, logDenial, and failureResponse flow so users get
an accurate message for global sessions.

In `@packages/control-plane/src/session/http/handlers/pull-request.handler.ts`:
- Around line 56-74: Move the repository-target validation in
pull-request.handler’s request flow so it runs before
getPromptingParticipantForPR/resolveAuthForPR, and change the guard to only
require repo_owner and repo_name. Remove base_branch from the required-session
check so SessionPullRequestService can continue falling back to
repoInfo.defaultBranch, and keep the existing 400 response for repo-less
sessions at that earlier boundary.

---

Nitpick comments:
In `@packages/control-plane/src/routes/automations.test.ts`:
- Around line 214-246: The no_repository automation test only verifies persisted
null repo fields and could miss an accidental SCM lookup in
handleCreateAutomation(). Update the no_repository test case in
automations.test.ts to also assert the repo resolver path is not used by
checking the resolver mock (the resolveRepoOrError-related mock) was not called.
Keep the existing create assertions, and add the negative call assertion in the
no_repository scenario so regressions that still resolve SCM access are caught.

In `@packages/control-plane/src/routes/slack-notify.test.ts`:
- Line 13: Reset the mocked getGlobal between tests so one case’s resolved value
does not leak into later cases. Update the slack-notify test setup around
getGlobal and the affected scenarios in handleSlackNotify to restore the mock
implementation in test teardown or reinitialize it per test, since
vi.clearAllMocks only clears calls and not return values.

In `@packages/control-plane/src/sandbox/lifecycle/manager.test.ts`:
- Around line 503-513: The repo-null spawn test in manager.test.ts should also
assert the new sandboxId fallback used by doSpawn(). Update the expectation
around provider.createSandbox to verify that buildSandboxIdForSession() resolves
to session.id when repoOwner, repoName, and branch are null, so the test locks
down the repo-less session contract change.

In `@packages/control-plane/src/scheduler/durable-object.test.ts`:
- Around line 481-500: The durable-object test for /internal/tick only verifies
the failed run record and misses the case where a session could still be created
before failure. Update the test around createSchedulerDO and scheduler.fetch to
also assert that no session is created when mockCheckRepositoryAccess returns
null, using the relevant session-creation mock or assertion in this spec so
target-resolution failure is verified to abort before any session state is
created.

In `@packages/shared/src/types/index.ts`:
- Around line 771-805: Model Automation and CreateAutomationRequest as
discriminated unions keyed by targetMode instead of flat interfaces, so
TypeScript can enforce the two valid shapes. Update the Automation and
CreateAutomationRequest definitions in the shared types module to split
fixed_single_repo from no_repository, making
repoOwner/repoName/repoId/baseBranch required only where applicable and absent
or null where not. Keep targetMode as the discriminator and adjust any related
TriggerConfig typing or dependent fields so impossible combinations are no
longer representable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4388ed0-1bf8-4071-9bf0-ac1f7845eb28

📥 Commits

Reviewing files that changed from the base of the PR and between 061563d and 2f0462b.

📒 Files selected for processing (66)
  • docs/AUTOMATIONS.md
  • packages/control-plane/src/automation/target-resolution.test.ts
  • packages/control-plane/src/automation/target-resolution.ts
  • packages/control-plane/src/db/analytics-store.ts
  • packages/control-plane/src/db/automation-store.test.ts
  • packages/control-plane/src/db/automation-store.ts
  • packages/control-plane/src/db/mcp-servers.ts
  • packages/control-plane/src/db/session-index.ts
  • packages/control-plane/src/router.create-session.test.ts
  • packages/control-plane/src/router.spawn-child.test.ts
  • packages/control-plane/src/routes/automations.test.ts
  • packages/control-plane/src/routes/automations.ts
  • packages/control-plane/src/routes/session-child-spawn.ts
  • packages/control-plane/src/routes/session-create.ts
  • packages/control-plane/src/routes/slack-notify.test.ts
  • packages/control-plane/src/routes/slack-notify.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/sandbox/provider.ts
  • packages/control-plane/src/sandbox/providers/daytona-provider.ts
  • packages/control-plane/src/sandbox/providers/vercel/provider.ts
  • packages/control-plane/src/sandbox/sandbox-env.ts
  • packages/control-plane/src/scheduler/durable-object.test.ts
  • packages/control-plane/src/scheduler/durable-object.ts
  • packages/control-plane/src/session/create-session-input.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/http/handlers/child-sessions.handler.ts
  • packages/control-plane/src/session/http/handlers/pull-request.handler.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
  • packages/control-plane/src/session/http/handlers/session-lifecycle.handler.ts
  • packages/control-plane/src/session/initialize.ts
  • packages/control-plane/src/session/integration-settings-resolution.ts
  • packages/control-plane/src/session/openai-token-refresh-service.ts
  • packages/control-plane/src/session/pull-request-service.ts
  • packages/control-plane/src/session/repository.ts
  • packages/control-plane/src/session/schema.test.ts
  • packages/control-plane/src/session/schema.ts
  • packages/control-plane/src/session/types.ts
  • packages/control-plane/test/integration/analytics.test.ts
  • packages/control-plane/test/integration/scheduler-slack-events.test.ts
  • packages/control-plane/test/integration/webhooks-slack.test.ts
  • packages/modal-infra/src/sandbox/manager.py
  • packages/modal-infra/src/web_api.py
  • packages/modal-infra/tests/test_sandbox_env_vars.py
  • packages/modal-infra/tests/test_web_api_create_sandbox.py
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
  • packages/sandbox-runtime/src/sandbox_runtime/types.py
  • packages/sandbox-runtime/tests/test_entrypoint_build_mode.py
  • packages/sandbox-runtime/tests/test_tool_installation.py
  • packages/shared/src/triggers/testing.ts
  • packages/shared/src/types/index.ts
  • packages/web/src/app/(app)/automations/[id]/edit/page.tsx
  • packages/web/src/app/(app)/automations/[id]/page.tsx
  • packages/web/src/components/automations/automation-form.test.tsx
  • packages/web/src/components/automations/automation-form.tsx
  • packages/web/src/components/automations/automations-list.tsx
  • packages/web/src/components/global-command-menu.tsx
  • packages/web/src/components/session-header.tsx
  • packages/web/src/components/session-sidebar.tsx
  • packages/web/src/components/settings/data-controls-settings.tsx
  • packages/web/src/components/sidebar/child-sessions-section.tsx
  • packages/web/src/components/sidebar/metadata-section.tsx
  • packages/web/src/lib/repo-label.ts
  • terraform/d1/migrations/0029_allow_no_repository_targets.sql

Comment thread packages/control-plane/src/db/automation-store.ts Outdated
Comment thread packages/control-plane/src/db/session-index.ts Outdated
Comment thread packages/control-plane/src/routes/session-create.ts Outdated
Comment thread packages/control-plane/src/sandbox/providers/daytona-provider.ts Outdated
Comment thread packages/control-plane/src/sandbox/providers/vercel/provider.ts Outdated
Comment thread packages/control-plane/src/session/schema.ts Outdated
Comment thread packages/modal-infra/src/sandbox/manager.py Outdated
Comment thread packages/web/src/components/session-header.tsx
Comment thread packages/web/src/components/sidebar/metadata-section.tsx
Co-authored-by: Orca <help@stably.ai>
@rubenlangeweg

Copy link
Copy Markdown
Contributor Author

Skipping the CodeRabbit nitpicks intentionally.

  • Extra assertion-only tests in automations, Slack notify, sandbox lifecycle, and scheduler specs: not worth the added test churn for this fix.
  • Shared Automation/CreateAutomationRequest discriminated unions: useful cleanup, but it spreads type churn across packages and is not required for the no-repository behavior fix.

The actual behavior fixes are committed in d76702a and covered by the final CONTRIBUTING.md checks.

@ColeMurray

Copy link
Copy Markdown
Owner

@rubenlangeweg can you include a few screenshots of the UI for this

@rubenlangeweg

Copy link
Copy Markdown
Contributor Author

@ColeMurray attached some screenshots and a recording.

CleanShot 2026-06-26 at 23 11 01@2x CleanShot 2026-06-26 at 23 11 08@2x CleanShot 2026-06-26 at 23 11 27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/control-plane/src/sandbox/lifecycle/manager.test.ts (1)

1300-1305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the timeout fixtures into named ...Ms constants.

The inline 10000 and 11 * 60 * 1000 literals make the test harder to scan and drift from the repo’s duration rule for TypeScript. Use named millisecond constants so the units stay explicit in the fixture setup.

Suggested change
     it("does not explicitly stop providers when the capability is disabled", async () => {
+      const HEARTBEAT_AGE_MS = 10_000;
+      const INACTIVITY_AGE_MS = 11 * 60 * 1000;
       const now = Date.now();
       const sandbox = createMockSandbox({
         status: "ready",
-        last_heartbeat: now - 10000,
-        last_activity: now - 11 * 60 * 1000,
+        last_heartbeat: now - HEARTBEAT_AGE_MS,
+        last_activity: now - INACTIVITY_AGE_MS,
       });

As per coding guidelines, For durations and timeouts: use seconds for Python, milliseconds for TypeScript, encode the unit in variable names (Python: timeout_seconds, TypeScript: timeoutMs, INACTIVITY_TIMEOUT_MS), define each default value exactly once in a named constant, and do not restate literal values in comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/sandbox/lifecycle/manager.test.ts` around lines
1300 - 1305, The sandbox fixture in manager.test.ts uses inline millisecond
durations that should be named constants for clarity and consistency. Replace
the literal heartbeat and inactivity values in the createMockSandbox setup with
explicit TypeScript millisecond constants (for example, timeoutMs-style names)
defined once nearby, and reference those constants in the fixture so the units
stay obvious and the duration rule is followed.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/control-plane/src/sandbox/lifecycle/manager.test.ts`:
- Around line 1300-1305: The sandbox fixture in manager.test.ts uses inline
millisecond durations that should be named constants for clarity and
consistency. Replace the literal heartbeat and inactivity values in the
createMockSandbox setup with explicit TypeScript millisecond constants (for
example, timeoutMs-style names) defined once nearby, and reference those
constants in the fixture so the units stay obvious and the duration rule is
followed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 820b9974-1bf3-4c7e-9932-63b820ce6df5

📥 Commits

Reviewing files that changed from the base of the PR and between 8f62dc0 and a66ade8.

📒 Files selected for processing (5)
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
  • packages/sandbox-runtime/tests/test_tool_installation.py
  • packages/shared/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/sandbox-runtime/tests/test_tool_installation.py
  • packages/shared/src/types/index.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py

Comment thread packages/control-plane/src/session/schema.ts Outdated
Comment thread terraform/d1/migrations/0029_allow_no_repository_targets.sql Outdated
@ColeMurray

Copy link
Copy Markdown
Owner

[Automated Review] 🟡 Architecture (non-blocking): consider a discriminated union for Automation instead of widening repo fields to | null.

A design call worth making consciously before merge — not a blocker.

The PR widens repoOwner / repoName / baseBranch to string | null across Session / SessionState / Automation / ChildSessionDetail (packages/shared/src/types/index.ts). Because Automation already carries the targetMode discriminant, independent-nullable modeling lets illegal states be representable at the type layer — e.g. { targetMode: "no_repository", repoOwner: "x" } or { targetMode: "fixed_single_repo", repoOwner: null } — so the "both repo fields exist, or neither does" invariant ends up enforced only by scattered runtime guards.

Consequences visible in this diff:

  • The repo-presence predicate !repo_owner || !repo_name is hand-rolled across ~11 control-plane files; the one type guard introduced (sessionHasRepository, sandbox/lifecycle/manager.ts:190) isn't exported and isn't even reused in its own file (resolveAgentSlackNotifyEnabled re-inlines it).
  • The "fixed_single_repo" default + normalize/validate logic is duplicated across the store mapper (automationTargetMode), target-resolution.ts, and the route; toAutomation() throws per-row on an unexpected target_mode, so a single bad row 500s the entire automations list endpoint.
  • "No repository" is hardcoded ~8× even though NO_REPOSITORY_LABEL / formatRepoLabel now exists (and isn't used in its own package).

Since targetMode already exists and toAutomation() already branches on it, a sum type is nearly free and lets every consumer narrow on the discriminant to obtain non-null repo fields, deleting most downstream null checks:

type Automation = AutomationBase & (
  | { targetMode: "fixed_single_repo"; repoOwner: string; repoName: string; baseBranch: string; repoId: number | null }
  | { targetMode: "no_repository"; repoOwner: null; repoName: null; baseBranch: null; repoId: null }
);

Recommendation: do this now while it's cheap. The PR is framed as the first #833 slice with future dynamic repo discovery — without consolidating here, the next slice repeats the null-handling sweep across the same files. Sessions are a different case: a session's repo-ness is a derived fact, so keeping them nullable + one shared exported sessionHasRepository guard (rather than adding a target_mode column) is the right asymmetry.

Relatedly: resolveAutomationTarget(env, automation) (automation/target-resolution.ts) can't see the trigger event, but dynamic discovery will need it — and the event payload is already in scope at the scheduler/durable-object.ts call site. Threading a triggerContext parameter now (even if unused by today's two modes) would make the future mode an additive branch rather than a signature change to the seam this PR is introducing.

Co-authored-by: Orca <help@stably.ai>
@rubenlangeweg

rubenlangeweg commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

consider a discriminated union for Automation instead of widening repo fields to | null.

Done in 69d7696 and fd80ade.

Automation and CreateAutomationRequest are now discriminated unions on targetMode. Repo-backed automations require repo fields. no_repository automations require null repo fields.

I also moved sessionHasRepository into the shared session types for control-plane code and reused it in the lifecycle manager. The web package now uses its existing NO_REPOSITORY_LABEL constant instead of hardcoding that label in the touched UI.

I did not add triggerContext yet. The current modes do not use event payload, so that would be a dead parameter. We can add it in the dynamic discovery slice when there is a caller that needs it.

Co-authored-by: Orca <help@stably.ai>
@rubenlangeweg rubenlangeweg requested a review from ColeMurray June 28, 2026 06:38
Comment thread packages/control-plane/src/db/analytics-store.ts Outdated
Comment thread packages/control-plane/src/routes/slack-notify.ts Outdated
Comment thread packages/control-plane/src/sandbox/lifecycle/manager.ts Outdated
"slack-notify.js": "AGENT_SLACK_NOTIFY_ENABLED",
}

AGENT_TOOLS_REQUIRING_REPOSITORY = {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these seem like they should be able to run without the repository

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ColeMurray , for now made a conservative change: status/change tools now install without a repo, while spanw-taks remains repo-gated because the child-spawn control plane path still requires a repo target.

do you want this PR to also support spawning no-repo child sessions end-to-end, or keeping spawn repo-gated the right boundary ?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it doesn't introduce several widespread changes, let's proceed updating to allow no-repo child sessions. If it does cause a larger refactor, we can handle in a follow-up PR

@rubenlangeweg rubenlangeweg Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets finish/address this no-repo child session AFTER the remove targetMode.

@rubenlangeweg rubenlangeweg requested a review from ColeMurray June 29, 2026 06:10
@ColeMurray

Copy link
Copy Markdown
Owner

I think targetMode is the wrong durable abstraction here, especially looking ahead to multi-repo support.

This PR currently introduces repository presence as an automation mode (fixed_single_repo vs no_repository), and the follow-on design extends that into cardinality (fixed_single_repo, fixed_multi_repo, no_repository). That feels like mode explosion: absence of repository context, number of repositories, trigger compatibility, and run materialization strategy all get packed into one enum.

I’d rather model this as optional repository context:

  • session creation can omit repository context to create a repo-less session
  • automations can store zero, one, or many repository targets
  • event-scoped triggers can derive or require repository context at the trigger boundary
  • the scheduler can derive launch behavior from resolved session launches:
    • zero repos: one repo-less session
    • one repo: one repo-backed session
    • multiple repos: one session per repo, grouped for history

That keeps “target” scoped to automation configuration and avoids passing repo mode through session/sandbox/runtime layers. The normalized target rows planned for multi-repo still seem useful, but I don’t think target_mode should be the authority. A helper like resolveAutomationSessionLaunches(automation) returning one or more session launch specs would give the scheduler the behavior it needs without making fixed_single_repo / fixed_multi_repo / no_repository part of the product/data model.

@rubenlangeweg

Copy link
Copy Markdown
Contributor Author

The recent commit addresses the direction change with focus on supporting zero or one repository, NOT many. PR #850 would need to be refactored/updated from targetMode to also align with the direction change and support many.

I think targetMode is the wrong durable abstraction here, especially looking ahead to multi-repo support.

This PR currently introduces repository presence as an automation mode (fixed_single_repo vs no_repository), and the follow-on design extends that into cardinality (fixed_single_repo, fixed_multi_repo, no_repository). That feels like mode explosion: absence of repository context, number of repositories, trigger compatibility, and run materialization strategy all get packed into one enum.

I’d rather model this as optional repository context:

  • session creation can omit repository context to create a repo-less session

  • automations can store zero, one, or many repository targets

  • event-scoped triggers can derive or require repository context at the trigger boundary

  • the scheduler can derive launch behavior from resolved session launches:

    • zero repos: one repo-less session
    • one repo: one repo-backed session
    • multiple repos: one session per repo, grouped for history

That keeps “target” scoped to automation configuration and avoids passing repo mode through session/sandbox/runtime layers. The normalized target rows planned for multi-repo still seem useful, but I don’t think target_mode should be the authority. A helper like resolveAutomationSessionLaunches(automation) returning one or more session launch specs would give the scheduler the behavior it needs without making fixed_single_repo / fixed_multi_repo / no_repository part of the product/data model.

ColeMurray added a commit that referenced this pull request Jun 30, 2026
## Summary

Builds on PR #836 and keeps its commits in this branch history so the
original work remains visible and attributed. This adds one follow-up
cleanup commit that finishes the repo-less architecture pass we
discussed.

- replaces the automation launch/target abstraction with a nullable
repository-context resolver
- enforces repo/base-branch invariants in request schemas, D1 migration,
automation storage, and session indexing
- allows repo-less child sessions while preventing children from adding
or switching repository context
- removes repository-mode/no-repository mode naming from Modal and
sandbox runtime paths
- updates provider/settings copy away from repository-targeting language

## Notes

This PR intentionally focuses on the current repo-less automation design
from #836. Multi-repo automation/session design from #850 should be
handled separately after the repo-less boundary is settled.

## Validation

- npm run build -w @open-inspect/shared
- npm test -w @open-inspect/shared -- src/types/boundary-schemas.test.ts
- npm test -w @open-inspect/control-plane --
src/automation/repository.test.ts src/db/automation-store.test.ts
src/db/session-index.test.ts src/session/create-session-input.test.ts
- npm test -w @open-inspect/control-plane --
src/routes/automations.test.ts src/router.spawn-child.test.ts
src/session/http/handlers/child-sessions.handler.test.ts
src/session/http/handlers/pull-request.handler.test.ts
src/session/http/handlers/sandbox.handler.test.ts
src/session/http/handlers/session-lifecycle.handler.test.ts
src/scheduler/durable-object.test.ts
- npm test -w @open-inspect/web --
src/components/automations/automation-form.test.tsx
- npm run typecheck -w @open-inspect/shared
- npm run typecheck -w @open-inspect/control-plane
- npm run typecheck -w @open-inspect/web
- npm run lint -w @open-inspect/shared
- npm run lint -w @open-inspect/control-plane
- npm run lint -w @open-inspect/web
- cd packages/modal-infra && PYTHONPATH=../sandbox-runtime/src:. pytest
tests/test_sandbox_env_vars.py tests/test_web_api_create_sandbox.py -v
- cd packages/sandbox-runtime && PYTHONPATH=src pytest
tests/test_entrypoint_build_mode.py tests/test_tool_installation.py -v
- cd packages/modal-infra && ruff check src/sandbox/manager.py
tests/test_sandbox_env_vars.py tests/test_web_api_create_sandbox.py
- cd packages/sandbox-runtime && ruff check
src/sandbox_runtime/bridge.py src/sandbox_runtime/entrypoint.py
tests/test_entrypoint_build_mode.py tests/test_tool_installation.py


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for “No repository” sessions and automations with
conditional repository configuration and “No repository” display across
the UI.
* **Bug Fixes**
* Improved end-to-end handling of nullable/partial repository fields,
including automation/session create & update validation, PR/SCM
credential gating, Slack behavior, analytics grouping, and
sandbox/no-repo runtime behavior.
* **Documentation**
* Updated automation setup docs to describe the new optional repository
configuration flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ruben Langeweg <rubenlangeweg@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Ruben Langeweg <34856866+rubenlangeweg@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants