Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions .github/workflows/jules-preflight-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Jules Preflight Tests

on:
pull_request:
paths:
- 'scripts/jules.py'
- 'scripts/jules_preflight.py'
- 'tests/test_jules_preflight.py'
- '.github/workflows/jules-preflight.yml'
- '.github/workflows/jules-preflight-tests.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Compile dispatcher scripts
run: python3 -m py_compile scripts/jules.py scripts/jules_preflight.py
- name: Run preflight safety tests
run: python3 -m unittest -v tests.test_jules_preflight
42 changes: 42 additions & 0 deletions .github/workflows/jules-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Jules Readiness Preflight
run-name: Jules preflight by ${{ github.actor }}

on:
workflow_dispatch:
inputs:
target:
description: "Approved opaque target ID"
required: true
default: "phase0"
type: string
task:
description: "Approved task ID"
required: true
default: "phase0-private-repo-smoke-test"
type: string

permissions:
contents: read

jobs:
preflight:
# Match the dispatch actor gate. The environment retains the existing
# credential/policy boundary; this workflow performs no provider mutation.
if: github.actor == 'mark-e-deyoung'
runs-on: ubuntu-latest
timeout-minutes: 2
environment: jules-dispatch
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- name: Check approved target/task and Jules Source readiness
env:
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
JULES_TARGETS_JSON: ${{ secrets.JULES_TARGETS_JSON }}
TARGET_ID: ${{ inputs.target }}
TASK_ID: ${{ inputs.task }}
run: |
python3 scripts/jules_preflight.py \
--target "$TARGET_ID" \
--task "$TASK_ID"
44 changes: 44 additions & 0 deletions scripts/jules_preflight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Sanitized, non-mutating readiness preflight for approved Jules dispatch inputs.

The preflight deliberately reuses the hardened dispatcher policy and provider
source-resolution functions. It validates only that the opaque target and task
are admitted and that the target has exactly one Jules Source. It never creates
a Jules session and never prints repository, branch, source, prompt, credential,
or provider metadata.
"""

from __future__ import annotations

import argparse
import json

from jules import load_task, resolve_source, resolve_target


def preflight(target: str, task: str) -> dict:
repository, _branch = resolve_target(target)
load_task(task)
resolve_source(repository)
return {
"ready": True,
"target": target,
"task": task,
"target_policy": "approved",
"task_policy": "approved",
"jules_source": "available-unique",
"session_created": False,
"authority_mutation": False,
}


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", required=True, help="approved opaque target ID")
parser.add_argument("--task", required=True, help="approved task ID")
args = parser.parse_args()
print(json.dumps(preflight(args.target, args.task), indent=2, sort_keys=True))


if __name__ == "__main__":
main()
76 changes: 76 additions & 0 deletions tests/test_jules_preflight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import patch


ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
sys.path.insert(0, str(SCRIPTS))

import jules # noqa: E402
import jules_preflight # noqa: E402


class JulesPreflightTests(unittest.TestCase):
def test_preflight_returns_only_sanitized_readiness(self):
private_repo = "private-owner/secret-repository"
private_branch = "secret-branch"
private_source = {"name": "sources/secret-provider-id", "githubRepo": {"owner": "private-owner", "repo": "secret-repository"}}

with (
patch.object(jules_preflight, "resolve_target", return_value=(private_repo, private_branch)),
patch.object(jules_preflight, "load_task", return_value="PRIVATE PROMPT CONTENT"),
patch.object(jules_preflight, "resolve_source", return_value=private_source),
):
result = jules_preflight.preflight("opaque-target", "approved-task")

self.assertEqual(
result,
{
"ready": True,
"target": "opaque-target",
"task": "approved-task",
"target_policy": "approved",
"task_policy": "approved",
"jules_source": "available-unique",
"session_created": False,
"authority_mutation": False,
},
)
rendered = repr(result)
self.assertNotIn(private_repo, rendered)
self.assertNotIn(private_branch, rendered)
self.assertNotIn("secret-provider-id", rendered)
self.assertNotIn("PRIVATE PROMPT CONTENT", rendered)

def test_preflight_does_not_call_provider_session_creation(self):
with (
patch.object(jules_preflight, "resolve_target", return_value=("owner/repo", "main")),
patch.object(jules_preflight, "load_task", return_value="task"),
patch.object(jules_preflight, "resolve_source", return_value={"name": "sources/1"}),
patch.object(jules, "request") as provider_request,
):
result = jules_preflight.preflight("opaque-target", "approved-task")

self.assertTrue(result["ready"])
provider_request.assert_not_called()

def test_real_target_policy_rejects_unapproved_alias_without_disclosure(self):
old = os.environ.get("JULES_TARGETS_JSON")
os.environ["JULES_TARGETS_JSON"] = '{"approved":{"repository":"owner/private","branch":"main"}}'
try:
with self.assertRaises(SystemExit) as raised:
jules.resolve_target("not-approved")
self.assertEqual(str(raised.exception), "Target is not approved")
self.assertNotIn("owner/private", str(raised.exception))
finally:
if old is None:
os.environ.pop("JULES_TARGETS_JSON", None)
else:
os.environ["JULES_TARGETS_JSON"] = old


if __name__ == "__main__":
unittest.main()
Loading