mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Merge branch 'main' into jchu/codex-cli-owner-nudge-analytics-2026.04.22
This commit is contained in:
@@ -29,7 +29,6 @@ common:linux --test_env=PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
common:macos --test_env=PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
# Pass through some env vars Windows needs to use powershell?
|
||||
common:windows --test_env=PATH
|
||||
common:windows --test_env=SYSTEMROOT
|
||||
common:windows --test_env=COMSPEC
|
||||
common:windows --test_env=WINDIR
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new,*meriyah.umd.min.js
|
||||
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new
|
||||
check-hidden = true
|
||||
ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b
|
||||
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE,PASE,SEH
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: codex-issue-digest
|
||||
description: Run a GitHub issue digest for openai/codex by feature-area labels, all areas, and configurable time windows. Use when asked to summarize recent Codex bug reports or enhancement requests, especially for owner-specific labels such as tui, exec, app, or similar areas.
|
||||
---
|
||||
|
||||
# Codex Issue Digest
|
||||
|
||||
## Objective
|
||||
|
||||
Produce a concise, insight-oriented digest of `openai/codex` issues for the requested feature-area labels over the previous 24 hours by default. Honor a different duration when the user asks for one, for example "past week" or "48 hours".
|
||||
|
||||
Include only issues that currently have `bug` or `enhancement` plus at least one requested owner label. If the user asks for all areas or all labels, collect `bug`/`enhancement` issues across all labels.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Feature-area labels, for example `tui exec`
|
||||
- `all areas` / `all labels` to scan all current feature labels
|
||||
- Optional repo override, default `openai/codex`
|
||||
- Optional time window, default previous 24 hours; examples: `48h`, `7d`, `1w`, `past week`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Run the collector from a current Codex repo checkout:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/codex-issue-digest/scripts/collect_issue_digest.py --labels tui exec --window-hours 24
|
||||
```
|
||||
|
||||
Use `--window "past week"` or `--window-hours 168` when the user asks for a non-default duration. Use `--all-labels` when the user says all areas or all labels.
|
||||
|
||||
2. Use the JSON as the source of truth. It includes new issues, new issue comments, new reactions/upvotes, current labels, current reaction counts, model-ready `summary_inputs`, and detailed `digest_rows`.
|
||||
3. Start the report with `## Summary`, then `## Details`.
|
||||
4. In `## Summary`, write skim-first headlines:
|
||||
- Lead with the most important fact or judgment. Do not start with aggregate counts unless the aggregate itself is the story.
|
||||
- Make the first 1-3 bullets answer "what should owners pay attention to right now?"
|
||||
- Bold only the critical insight phrase in each high-priority bullet, for example `**GPT-5.5 context is the dominant pressure point**`.
|
||||
- Keep summary bullets short enough to scan in about 20 seconds.
|
||||
- Put broad stats near the end of the summary, after the owner-relevant takeaways.
|
||||
- Say clearly when there is nothing significant to act on.
|
||||
- Call out any areas or themes receiving lots of user attention.
|
||||
- Cluster and name themes yourself from `summary_inputs`; the collector intentionally does not hard-code issue categories.
|
||||
- Use a cluster only when the issues genuinely share the same product problem. If several issues merely share a broad platform or label, describe them individually.
|
||||
- Do not omit a repeated theme just because its individual issues fall below the details table cutoff. Several similar reports should be called out as a repeated customer concern.
|
||||
- For single-issue rows, summarize the concern directly instead of calling it a cluster.
|
||||
- Use inline numbered issue links from each relevant row's `ref_markdown`.
|
||||
5. In `## Details`, include a compact table only when useful:
|
||||
- Prefer rows from `digest_rows`; include a `Refs` column using each row's `ref_markdown`.
|
||||
- Keep the table short; omit low-signal rows when the summary already covers them.
|
||||
- Use compact columns such as marker, area, type, description, interactions, and refs.
|
||||
- The `Description` cell should be a short owner-readable phrase. Use row `description`, title, body excerpts, and recent comments, but do not mechanically copy the raw GitHub issue title when it contains incidental details.
|
||||
- A clear quiet/no-concern sentence when there is no meaningful signal.
|
||||
6. Use the JSON `attention_marker` exactly. It is empty for normal rows, `🔥` for elevated rows, and `🔥🔥` for very high-attention rows. The actual cutoffs are in `attention_thresholds`.
|
||||
7. Use inline numbered references where a row or bullet points to issues, for example `Compaction bugs [1](https://github.com/openai/codex/issues/123), [2](https://github.com/openai/codex/issues/456)`. Do not add a separate footnotes section.
|
||||
8. Label `interactions` as `Interactions`; it counts posts/comments/reactions during the requested window, not unique people.
|
||||
9. Mention the collector `script_version`, repo checkout `git_head`, and time window in the digest footer or final line.
|
||||
|
||||
## Reaction Handling
|
||||
|
||||
The collector uses GitHub reactions endpoints, which include `created_at`, to count reactions created during the digest window for hydrated issues. It reports both in-window reaction counts and current reaction totals. Treat current reaction totals as standing engagement, and treat `new_reactions` / `new_upvotes` as windowed activity.
|
||||
|
||||
By default, the collector fetches issue comments with `since=<window start>` and caps the number of comment pages per issue. This keeps very long historical threads from dominating a digest run and focuses the report on recent posts. Use `--fetch-all-comments` only when exhaustive comment history is more important than runtime.
|
||||
|
||||
GitHub issue search is still seeded by issue `updated_at`, so a purely reaction-only issue may be missed if reactions do not bump `updated_at`. Covering every reaction-only case would require either a persisted snapshot store or a broader scan of labeled issues.
|
||||
|
||||
## Attention Markers
|
||||
|
||||
The collector scales attention markers by the requested time window. The baseline is 10 human user interactions for `🔥` and 20 for `🔥🔥` over 24 hours; longer or shorter windows scale those cutoffs linearly and round up. For example, a one-week report uses 70 and 140 interactions. Human user interactions are human-authored new issue posts, human-authored new comments, and human reactions created during the window, including upvotes. Bot posts and bot reactions are excluded. In prose, explain this as high user interaction rather than naming the emoji.
|
||||
|
||||
## Freshness
|
||||
|
||||
The automation should run from a repo checkout that contains this skill. For shared daily use, prefer one of these patterns:
|
||||
|
||||
- Run the automation in a checkout that is refreshed before the automation starts, for example with `git pull --ff-only`.
|
||||
- If the automation cannot safely mutate the checkout, have it report the current `git_head` from the collector output so readers know which skill/script version produced the digest.
|
||||
|
||||
## Sample Owner Prompt
|
||||
|
||||
```text
|
||||
Use $codex-issue-digest to run the Codex issue digest for labels tui and exec over the previous 24 hours.
|
||||
```
|
||||
|
||||
```text
|
||||
Use $codex-issue-digest to run the Codex issue digest for all areas over the past week.
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Dry run the collector against recent issues:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/codex-issue-digest/scripts/collect_issue_digest.py --labels tui exec --window-hours 24
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/codex-issue-digest/scripts/collect_issue_digest.py --all-labels --window "past week" --limit-issues 10
|
||||
```
|
||||
|
||||
Run the focused script tests:
|
||||
|
||||
```bash
|
||||
pytest .codex/skills/codex-issue-digest/scripts/test_collect_issue_digest.py
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Codex Issue Digest"
|
||||
short_description: "Summarize Codex issues by labels or all areas"
|
||||
default_prompt: "Use $codex-issue-digest to run the Codex issue digest for labels tui and exec over the previous 24 hours."
|
||||
@@ -0,0 +1,988 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect recent openai/codex issue activity for owner-focused digests."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
SCRIPT_VERSION = 2
|
||||
QUALIFYING_KIND_LABELS = ("bug", "enhancement")
|
||||
REACTION_KEYS = ("+1", "-1", "laugh", "hooray", "confused", "heart", "rocket", "eyes")
|
||||
BASE_ATTENTION_WINDOW_HOURS = 24.0
|
||||
ONE_ATTENTION_INTERACTION_THRESHOLD = 10
|
||||
TWO_ATTENTION_INTERACTION_THRESHOLD = 20
|
||||
ALL_LABEL_PHRASES = {"all", "all areas", "all labels", "all-areas", "all-labels", "*"}
|
||||
|
||||
|
||||
class GhCommandError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Collect recent GitHub issue activity for a Codex owner digest."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo", default="openai/codex", help="OWNER/REPO, default openai/codex"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--labels",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Feature-area labels owned by the digest recipient, for example: tui exec",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-labels",
|
||||
action="store_true",
|
||||
help="Collect bug/enhancement issues across all feature-area labels",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window",
|
||||
help='Lookback duration such as "24h", "7d", "1w", or "past week"',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-hours", type=float, default=24.0, help="Lookback window"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--since", help="UTC ISO timestamp override for the window start"
|
||||
)
|
||||
parser.add_argument("--until", help="UTC ISO timestamp override for the window end")
|
||||
parser.add_argument(
|
||||
"--limit-issues",
|
||||
type=int,
|
||||
default=200,
|
||||
help="Maximum candidate issues to hydrate after search",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--body-chars", type=int, default=1200, help="Issue body excerpt length"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--comment-chars", type=int, default=900, help="Comment excerpt length"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-comment-pages",
|
||||
type=int,
|
||||
default=3,
|
||||
help=(
|
||||
"Maximum pages of issue comments to hydrate per issue after applying the "
|
||||
"window filter. Use 0 with --fetch-all-comments for no page cap."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fetch-all-comments",
|
||||
action="store_true",
|
||||
help="Hydrate complete issue comment histories instead of only window-updated comments.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_timestamp(value, arg_name):
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
return None
|
||||
if normalized.endswith("Z"):
|
||||
normalized = f"{normalized[:-1]}+00:00"
|
||||
try:
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
except ValueError as err:
|
||||
raise ValueError(f"{arg_name} must be an ISO timestamp") from err
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def format_timestamp(value):
|
||||
return (
|
||||
value.astimezone(timezone.utc)
|
||||
.replace(microsecond=0)
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z")
|
||||
)
|
||||
|
||||
|
||||
def resolve_window(args):
|
||||
until = parse_timestamp(args.until, "--until") or datetime.now(timezone.utc)
|
||||
since = parse_timestamp(args.since, "--since")
|
||||
if since is None:
|
||||
hours = parse_duration_hours(getattr(args, "window", None))
|
||||
if hours is None:
|
||||
hours = getattr(args, "window_hours", 24.0)
|
||||
if hours <= 0:
|
||||
raise ValueError("window duration must be > 0")
|
||||
since = until - timedelta(hours=hours)
|
||||
if since >= until:
|
||||
raise ValueError("--since must be before --until")
|
||||
return since, until
|
||||
|
||||
|
||||
def parse_duration_hours(value):
|
||||
if value is None:
|
||||
return None
|
||||
text = value.strip().casefold().replace("_", " ")
|
||||
if not text:
|
||||
return None
|
||||
text = re.sub(r"^(past|last)\s+", "", text)
|
||||
aliases = {
|
||||
"day": 24.0,
|
||||
"24h": 24.0,
|
||||
"week": 168.0,
|
||||
"7d": 168.0,
|
||||
}
|
||||
if text in aliases:
|
||||
return aliases[text]
|
||||
match = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)", text)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
match = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(d|day|days)", text)
|
||||
if match:
|
||||
return float(match.group(1)) * 24.0
|
||||
match = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(w|week|weeks)", text)
|
||||
if match:
|
||||
return float(match.group(1)) * 168.0
|
||||
raise ValueError(f"Unsupported duration: {value}")
|
||||
|
||||
|
||||
def normalize_requested_labels(labels, all_labels=False):
|
||||
out = []
|
||||
seen = set()
|
||||
for raw in labels:
|
||||
for piece in raw.split(","):
|
||||
label = piece.strip()
|
||||
if not label:
|
||||
continue
|
||||
key = label.casefold()
|
||||
if key not in seen:
|
||||
out.append(label)
|
||||
seen.add(key)
|
||||
phrase = " ".join(label.casefold() for label in out)
|
||||
if all_labels or phrase in ALL_LABEL_PHRASES:
|
||||
return [], True
|
||||
if not out:
|
||||
raise ValueError(
|
||||
"At least one feature-area label is required, or use --all-labels"
|
||||
)
|
||||
return out, False
|
||||
|
||||
|
||||
def quote_label(label):
|
||||
if re.fullmatch(r"[A-Za-z0-9_.:-]+", label):
|
||||
return f"label:{label}"
|
||||
escaped = label.replace('"', '\\"')
|
||||
return f'label:"{escaped}"'
|
||||
|
||||
|
||||
def build_search_queries(
|
||||
repo, owner_labels, since, kind_labels=QUALIFYING_KIND_LABELS, all_labels=False
|
||||
):
|
||||
since_date = since.date().isoformat()
|
||||
queries = []
|
||||
if all_labels:
|
||||
for kind_label in kind_labels:
|
||||
queries.append(
|
||||
" ".join(
|
||||
[
|
||||
f"repo:{repo}",
|
||||
"is:issue",
|
||||
f"updated:>={since_date}",
|
||||
quote_label(kind_label),
|
||||
]
|
||||
)
|
||||
)
|
||||
return queries
|
||||
for owner_label in owner_labels:
|
||||
for kind_label in kind_labels:
|
||||
queries.append(
|
||||
" ".join(
|
||||
[
|
||||
f"repo:{repo}",
|
||||
"is:issue",
|
||||
f"updated:>={since_date}",
|
||||
quote_label(owner_label),
|
||||
quote_label(kind_label),
|
||||
]
|
||||
)
|
||||
)
|
||||
return queries
|
||||
|
||||
|
||||
def _format_gh_error(cmd, err):
|
||||
stdout = (err.stdout or "").strip()
|
||||
stderr = (err.stderr or "").strip()
|
||||
parts = [f"GitHub CLI command failed: {' '.join(cmd)}"]
|
||||
if stdout:
|
||||
parts.append(f"stdout: {stdout}")
|
||||
if stderr:
|
||||
parts.append(f"stderr: {stderr}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def gh_json(args):
|
||||
cmd = ["gh", *args]
|
||||
try:
|
||||
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
except FileNotFoundError as err:
|
||||
raise GhCommandError("`gh` command not found") from err
|
||||
except subprocess.CalledProcessError as err:
|
||||
raise GhCommandError(_format_gh_error(cmd, err)) from err
|
||||
raw = proc.stdout.strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError as err:
|
||||
raise GhCommandError(
|
||||
f"Failed to parse JSON from gh output for {' '.join(args)}"
|
||||
) from err
|
||||
|
||||
|
||||
def gh_text(args):
|
||||
cmd = ["gh", *args]
|
||||
try:
|
||||
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
return ""
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
def git_head():
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "rev-parse", "--short=12", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
return None
|
||||
return proc.stdout.strip() or None
|
||||
|
||||
|
||||
def skill_relative_path():
|
||||
try:
|
||||
return str(Path(__file__).resolve().relative_to(Path.cwd().resolve()))
|
||||
except ValueError:
|
||||
return str(Path(__file__).resolve())
|
||||
|
||||
|
||||
def gh_api_list_paginated(endpoint, per_page=100, max_pages=None, with_metadata=False):
|
||||
items = []
|
||||
page = 1
|
||||
truncated = False
|
||||
while True:
|
||||
sep = "&" if "?" in endpoint else "?"
|
||||
page_endpoint = f"{endpoint}{sep}per_page={per_page}&page={page}"
|
||||
payload = gh_json(["api", page_endpoint])
|
||||
if payload is None:
|
||||
break
|
||||
if not isinstance(payload, list):
|
||||
raise GhCommandError(f"Unexpected paginated payload from gh api {endpoint}")
|
||||
items.extend(payload)
|
||||
if len(payload) < per_page:
|
||||
break
|
||||
if max_pages is not None and page >= max_pages:
|
||||
truncated = True
|
||||
break
|
||||
page += 1
|
||||
if with_metadata:
|
||||
return {
|
||||
"items": items,
|
||||
"truncated": truncated,
|
||||
"pages": page,
|
||||
"max_pages": max_pages,
|
||||
}
|
||||
return items
|
||||
|
||||
|
||||
def search_issue_numbers(queries, limit):
|
||||
numbers = {}
|
||||
for query in queries:
|
||||
page = 1
|
||||
while True:
|
||||
payload = gh_json(
|
||||
[
|
||||
"api",
|
||||
"search/issues",
|
||||
"-X",
|
||||
"GET",
|
||||
"-f",
|
||||
f"q={query}",
|
||||
"-f",
|
||||
"per_page=100",
|
||||
"-f",
|
||||
f"page={page}",
|
||||
]
|
||||
)
|
||||
if not isinstance(payload, dict):
|
||||
raise GhCommandError("Unexpected payload from GitHub issue search")
|
||||
items = payload.get("items") or []
|
||||
if not isinstance(items, list):
|
||||
raise GhCommandError("Expected search `items` to be a list")
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
number = item.get("number")
|
||||
if isinstance(number, int):
|
||||
numbers[number] = str(item.get("updated_at") or "")
|
||||
if len(items) < 100 or len(numbers) >= limit:
|
||||
break
|
||||
page += 1
|
||||
ordered = sorted(
|
||||
numbers, key=lambda number: (numbers[number], number), reverse=True
|
||||
)
|
||||
return ordered[:limit]
|
||||
|
||||
|
||||
def fetch_issue(repo, number):
|
||||
payload = gh_json(["api", f"repos/{repo}/issues/{number}"])
|
||||
if not isinstance(payload, dict):
|
||||
raise GhCommandError(f"Unexpected issue payload for #{number}")
|
||||
return payload
|
||||
|
||||
|
||||
def fetch_comments(repo, number, since=None, max_pages=None):
|
||||
endpoint = f"repos/{repo}/issues/{number}/comments"
|
||||
if since is not None:
|
||||
endpoint = f"{endpoint}?since={quote(format_timestamp(since), safe='')}"
|
||||
return gh_api_list_paginated(
|
||||
endpoint,
|
||||
max_pages=max_pages,
|
||||
with_metadata=True,
|
||||
)
|
||||
|
||||
|
||||
def fetch_reactions_for_item(endpoint, item):
|
||||
if reaction_summary(item)["total"] <= 0:
|
||||
return []
|
||||
return gh_api_list_paginated(endpoint)
|
||||
|
||||
|
||||
def fetch_comment_reactions(repo, comments):
|
||||
reactions_by_comment_id = {}
|
||||
for comment in comments:
|
||||
comment_id = comment.get("id")
|
||||
if comment_id in (None, ""):
|
||||
continue
|
||||
endpoint = f"repos/{repo}/issues/comments/{comment_id}/reactions"
|
||||
reactions_by_comment_id[comment_id] = fetch_reactions_for_item(
|
||||
endpoint, comment
|
||||
)
|
||||
return reactions_by_comment_id
|
||||
|
||||
|
||||
def extract_login(user_obj):
|
||||
if isinstance(user_obj, dict):
|
||||
return str(user_obj.get("login") or "")
|
||||
return ""
|
||||
|
||||
|
||||
def is_bot_login(login):
|
||||
return bool(login) and login.lower().endswith("[bot]")
|
||||
|
||||
|
||||
def is_human_user(user_obj):
|
||||
login = extract_login(user_obj)
|
||||
return bool(login) and not is_bot_login(login)
|
||||
|
||||
|
||||
def label_names(issue):
|
||||
labels = []
|
||||
for label in issue.get("labels") or []:
|
||||
if isinstance(label, dict) and label.get("name"):
|
||||
labels.append(str(label["name"]))
|
||||
return sorted(labels, key=str.casefold)
|
||||
|
||||
|
||||
def matching_labels(labels, requested):
|
||||
labels_by_key = {label.casefold(): label for label in labels}
|
||||
return [label for label in requested if label.casefold() in labels_by_key]
|
||||
|
||||
|
||||
def area_labels(labels):
|
||||
kind_keys = {label.casefold() for label in QUALIFYING_KIND_LABELS}
|
||||
return [label for label in labels if label.casefold() not in kind_keys]
|
||||
|
||||
|
||||
def attention_thresholds_for_window(window_hours):
|
||||
if window_hours <= 0:
|
||||
raise ValueError("window_hours must be > 0")
|
||||
window_hours = round(window_hours, 6)
|
||||
scale = window_hours / BASE_ATTENTION_WINDOW_HOURS
|
||||
elevated = max(1, math.ceil(ONE_ATTENTION_INTERACTION_THRESHOLD * scale))
|
||||
very_high = max(
|
||||
elevated + 1, math.ceil(TWO_ATTENTION_INTERACTION_THRESHOLD * scale)
|
||||
)
|
||||
return {
|
||||
"base_window_hours": BASE_ATTENTION_WINDOW_HOURS,
|
||||
"window_hours": round(window_hours, 3),
|
||||
"scale": round(scale, 3),
|
||||
"elevated": elevated,
|
||||
"very_high": very_high,
|
||||
}
|
||||
|
||||
|
||||
def attention_level_for(user_interactions, attention_thresholds=None):
|
||||
thresholds = attention_thresholds or attention_thresholds_for_window(
|
||||
BASE_ATTENTION_WINDOW_HOURS
|
||||
)
|
||||
if user_interactions >= thresholds["very_high"]:
|
||||
return 2
|
||||
if user_interactions >= thresholds["elevated"]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def attention_marker_for(user_interactions, attention_thresholds=None):
|
||||
return "🔥" * attention_level_for(user_interactions, attention_thresholds)
|
||||
|
||||
|
||||
def reaction_summary(item):
|
||||
reactions = item.get("reactions")
|
||||
if not isinstance(reactions, dict):
|
||||
return {"total": 0, "counts": {}}
|
||||
counts = {}
|
||||
for key in REACTION_KEYS:
|
||||
value = reactions.get(key, 0)
|
||||
if isinstance(value, int) and value:
|
||||
counts[key] = value
|
||||
total = reactions.get("total_count")
|
||||
if not isinstance(total, int):
|
||||
total = sum(counts.values())
|
||||
return {"total": total, "counts": counts}
|
||||
|
||||
|
||||
def reaction_event_summary(reactions, since, until):
|
||||
counts = {}
|
||||
total = 0
|
||||
for reaction in reactions or []:
|
||||
if not isinstance(reaction, dict):
|
||||
continue
|
||||
if not is_in_window(str(reaction.get("created_at") or ""), since, until):
|
||||
continue
|
||||
if not is_human_user(reaction.get("user")):
|
||||
continue
|
||||
content = str(reaction.get("content") or "")
|
||||
if not content:
|
||||
continue
|
||||
counts[content] = counts.get(content, 0) + 1
|
||||
total += 1
|
||||
return {
|
||||
"total": total,
|
||||
"counts": counts,
|
||||
"upvotes": counts.get("+1", 0),
|
||||
}
|
||||
|
||||
|
||||
def compact_text(value, limit):
|
||||
text = re.sub(r"\s+", " ", str(value or "")).strip()
|
||||
if limit <= 0:
|
||||
return ""
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return f"{text[: max(limit - 1, 0)].rstrip()}..."
|
||||
|
||||
|
||||
def clean_title_for_description(title):
|
||||
cleaned = re.sub(r"\s+", " ", str(title or "")).strip()
|
||||
cleaned = re.sub(
|
||||
r"^(codex(?: desktop| app|\.app| cli)?|desktop|windows codex app)\s*[:,-]\s*",
|
||||
"",
|
||||
cleaned,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
cleaned = re.sub(r"^on windows,\s*", "Windows: ", cleaned, flags=re.IGNORECASE)
|
||||
cleaned = cleaned.strip(" -:;")
|
||||
return compact_text(cleaned, 80) or "Issue needs owner review"
|
||||
|
||||
|
||||
def issue_description(issue):
|
||||
return clean_title_for_description(issue.get("title"))
|
||||
|
||||
|
||||
def is_in_window(timestamp, since, until):
|
||||
parsed = parse_timestamp(timestamp, "timestamp")
|
||||
if parsed is None:
|
||||
return False
|
||||
return since <= parsed < until
|
||||
|
||||
|
||||
def summarize_comment(
|
||||
comment, comment_chars, reaction_events=None, since=None, until=None
|
||||
):
|
||||
reactions = reaction_summary(comment)
|
||||
new_reactions = (
|
||||
reaction_event_summary(reaction_events, since, until)
|
||||
if since is not None and until is not None
|
||||
else {"total": 0, "counts": {}, "upvotes": 0}
|
||||
)
|
||||
human_user_interaction = is_human_user(comment.get("user"))
|
||||
return {
|
||||
"id": comment.get("id"),
|
||||
"author": extract_login(comment.get("user")),
|
||||
"author_association": str(comment.get("author_association") or ""),
|
||||
"created_at": str(comment.get("created_at") or ""),
|
||||
"updated_at": str(comment.get("updated_at") or ""),
|
||||
"url": str(comment.get("html_url") or ""),
|
||||
"human_user_interaction": human_user_interaction,
|
||||
"reactions": reactions["counts"],
|
||||
"reaction_total": reactions["total"],
|
||||
"new_reactions": new_reactions["total"],
|
||||
"new_upvotes": new_reactions["upvotes"],
|
||||
"new_reaction_counts": new_reactions["counts"],
|
||||
"body_excerpt": compact_text(comment.get("body"), comment_chars),
|
||||
}
|
||||
|
||||
|
||||
def summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
requested_labels,
|
||||
since,
|
||||
until,
|
||||
body_chars,
|
||||
comment_chars,
|
||||
issue_reaction_events=None,
|
||||
comment_reactions_by_id=None,
|
||||
all_labels=False,
|
||||
comments_hydration=None,
|
||||
attention_thresholds=None,
|
||||
):
|
||||
labels = label_names(issue)
|
||||
labels_by_key = {label.casefold() for label in labels}
|
||||
kind_labels = [
|
||||
label for label in QUALIFYING_KIND_LABELS if label.casefold() in labels_by_key
|
||||
]
|
||||
if all_labels:
|
||||
owner_labels = area_labels(labels) or ["unlabeled"]
|
||||
else:
|
||||
owner_labels = matching_labels(labels, requested_labels)
|
||||
if not kind_labels or not owner_labels:
|
||||
return None
|
||||
|
||||
updated_at = str(issue.get("updated_at") or "")
|
||||
if not is_in_window(updated_at, since, until):
|
||||
return None
|
||||
|
||||
new_issue = is_in_window(str(issue.get("created_at") or ""), since, until)
|
||||
comment_reactions_by_id = comment_reactions_by_id or {}
|
||||
new_comments = [
|
||||
summarize_comment(
|
||||
comment,
|
||||
comment_chars,
|
||||
reaction_events=comment_reactions_by_id.get(comment.get("id")),
|
||||
since=since,
|
||||
until=until,
|
||||
)
|
||||
for comment in comments
|
||||
if is_in_window(str(comment.get("created_at") or ""), since, until)
|
||||
]
|
||||
new_comments.sort(key=lambda item: (item["created_at"], str(item["id"])))
|
||||
|
||||
issue_reactions = reaction_summary(issue)
|
||||
issue_reaction_events_summary = reaction_event_summary(
|
||||
issue_reaction_events, since, until
|
||||
)
|
||||
comment_reaction_events_summary = reaction_event_summary(
|
||||
[
|
||||
reaction
|
||||
for reactions in comment_reactions_by_id.values()
|
||||
for reaction in reactions
|
||||
],
|
||||
since,
|
||||
until,
|
||||
)
|
||||
new_reactions = (
|
||||
issue_reaction_events_summary["total"]
|
||||
+ comment_reaction_events_summary["total"]
|
||||
)
|
||||
new_upvotes = (
|
||||
issue_reaction_events_summary["upvotes"]
|
||||
+ comment_reaction_events_summary["upvotes"]
|
||||
)
|
||||
all_comment_reaction_total = sum(
|
||||
reaction_summary(comment)["total"] for comment in comments
|
||||
)
|
||||
new_comment_reaction_total = sum(
|
||||
comment["reaction_total"] for comment in new_comments
|
||||
)
|
||||
new_issue_user_interaction = new_issue and is_human_user(issue.get("user"))
|
||||
new_comment_user_interactions = sum(
|
||||
1 for comment in new_comments if comment["human_user_interaction"]
|
||||
)
|
||||
user_interactions = (
|
||||
int(new_issue_user_interaction) + new_comment_user_interactions + new_reactions
|
||||
)
|
||||
attention_level = attention_level_for(user_interactions, attention_thresholds)
|
||||
attention_marker = attention_marker_for(user_interactions, attention_thresholds)
|
||||
updated_without_visible_new_post = (
|
||||
not new_issue and not new_comments and new_reactions == 0
|
||||
)
|
||||
|
||||
engagement_score = (
|
||||
len(new_comments) * 3
|
||||
+ new_reactions
|
||||
+ issue_reactions["total"]
|
||||
+ new_comment_reaction_total
|
||||
+ min(int(issue.get("comments") or len(comments) or 0), 10)
|
||||
)
|
||||
|
||||
return {
|
||||
"number": issue.get("number"),
|
||||
"title": str(issue.get("title") or ""),
|
||||
"description": issue_description(issue),
|
||||
"url": str(issue.get("html_url") or ""),
|
||||
"state": str(issue.get("state") or ""),
|
||||
"author": extract_login(issue.get("user")),
|
||||
"author_association": str(issue.get("author_association") or ""),
|
||||
"created_at": str(issue.get("created_at") or ""),
|
||||
"updated_at": updated_at,
|
||||
"labels": labels,
|
||||
"kind_labels": kind_labels,
|
||||
"owner_labels": owner_labels,
|
||||
"comments_total": int(issue.get("comments") or len(comments) or 0),
|
||||
"comments_hydration": comments_hydration
|
||||
or {
|
||||
"fetched": len(comments),
|
||||
"since": None,
|
||||
"truncated": False,
|
||||
"max_pages": None,
|
||||
},
|
||||
"issue_reactions": issue_reactions["counts"],
|
||||
"issue_reaction_total": issue_reactions["total"],
|
||||
"comment_reaction_total": all_comment_reaction_total,
|
||||
"new_comment_reaction_total": new_comment_reaction_total,
|
||||
"new_issue_reactions": issue_reaction_events_summary["total"],
|
||||
"new_issue_upvotes": issue_reaction_events_summary["upvotes"],
|
||||
"new_comment_reactions": comment_reaction_events_summary["total"],
|
||||
"new_comment_upvotes": comment_reaction_events_summary["upvotes"],
|
||||
"new_reactions": new_reactions,
|
||||
"new_upvotes": new_upvotes,
|
||||
"user_interactions": user_interactions,
|
||||
"attention": attention_level > 0,
|
||||
"attention_level": attention_level,
|
||||
"attention_marker": attention_marker,
|
||||
"engagement_score": engagement_score,
|
||||
"activity": {
|
||||
"new_issue": new_issue,
|
||||
"new_comments": len(new_comments),
|
||||
"new_human_comments": new_comment_user_interactions,
|
||||
"new_reactions": new_reactions,
|
||||
"new_upvotes": new_upvotes,
|
||||
"updated_without_visible_new_post": updated_without_visible_new_post,
|
||||
},
|
||||
"body_excerpt": compact_text(issue.get("body"), body_chars),
|
||||
"new_comments": new_comments,
|
||||
}
|
||||
|
||||
|
||||
def count_by_label(issues, labels):
|
||||
out = {}
|
||||
for label in labels:
|
||||
matching = [issue for issue in issues if label in issue["owner_labels"]]
|
||||
out[label] = {
|
||||
"issues": len(matching),
|
||||
"new_issues": sum(
|
||||
1 for issue in matching if issue["activity"]["new_issue"]
|
||||
),
|
||||
"new_comments": sum(
|
||||
issue["activity"]["new_comments"] for issue in matching
|
||||
),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def count_by_kind(issues):
|
||||
out = {}
|
||||
for kind in QUALIFYING_KIND_LABELS:
|
||||
matching = [issue for issue in issues if kind in issue["kind_labels"]]
|
||||
out[kind] = {
|
||||
"issues": len(matching),
|
||||
"new_issues": sum(
|
||||
1 for issue in matching if issue["activity"]["new_issue"]
|
||||
),
|
||||
"new_comments": sum(
|
||||
issue["activity"]["new_comments"] for issue in matching
|
||||
),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def hot_items(issues, limit=8):
|
||||
ranked = sorted(
|
||||
issues,
|
||||
key=lambda issue: (
|
||||
issue["attention"],
|
||||
issue["attention_level"],
|
||||
issue["user_interactions"],
|
||||
issue["engagement_score"],
|
||||
issue["activity"]["new_comments"],
|
||||
issue["issue_reaction_total"] + issue["comment_reaction_total"],
|
||||
issue["updated_at"],
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
return [
|
||||
{
|
||||
"number": issue["number"],
|
||||
"title": issue["title"],
|
||||
"url": issue["url"],
|
||||
"owner_labels": issue["owner_labels"],
|
||||
"kind_labels": issue["kind_labels"],
|
||||
"attention": issue["attention"],
|
||||
"attention_level": issue["attention_level"],
|
||||
"attention_marker": issue["attention_marker"],
|
||||
"user_interactions": issue["user_interactions"],
|
||||
"new_reactions": issue["new_reactions"],
|
||||
"new_upvotes": issue["new_upvotes"],
|
||||
"engagement_score": issue["engagement_score"],
|
||||
"new_comments": issue["activity"]["new_comments"],
|
||||
"reaction_total": issue["issue_reaction_total"]
|
||||
+ issue["comment_reaction_total"],
|
||||
}
|
||||
for issue in ranked[:limit]
|
||||
if issue["engagement_score"] > 0
|
||||
]
|
||||
|
||||
|
||||
def ranked_digest_issues(issues):
|
||||
return sorted(
|
||||
issues,
|
||||
key=lambda issue: (
|
||||
issue["attention"],
|
||||
issue["attention_level"],
|
||||
issue["user_interactions"],
|
||||
issue["engagement_score"],
|
||||
issue["activity"]["new_comments"],
|
||||
issue["updated_at"],
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def digest_rows(issues, limit=10, ref_map=None):
|
||||
ranked = ranked_digest_issues(issues)
|
||||
if ref_map is None:
|
||||
ref_map = {issue["number"]: ref for ref, issue in enumerate(ranked, start=1)}
|
||||
rows = []
|
||||
for issue in ranked[:limit]:
|
||||
ref = ref_map[issue["number"]]
|
||||
reaction_total = issue["issue_reaction_total"] + issue["comment_reaction_total"]
|
||||
rows.append(
|
||||
{
|
||||
"ref": ref,
|
||||
"ref_markdown": f"[{ref}]({issue['url']})",
|
||||
"marker": issue["attention_marker"],
|
||||
"attention_marker": issue["attention_marker"],
|
||||
"number": issue["number"],
|
||||
"description": issue["description"],
|
||||
"title": issue["title"],
|
||||
"url": issue["url"],
|
||||
"area": ", ".join(issue["owner_labels"]),
|
||||
"kind": ", ".join(issue["kind_labels"]),
|
||||
"state": issue["state"],
|
||||
"interactions": issue["user_interactions"],
|
||||
"user_interactions": issue["user_interactions"],
|
||||
"new_reactions": issue["new_reactions"],
|
||||
"new_upvotes": issue["new_upvotes"],
|
||||
"current_reactions": reaction_total,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def issue_ref_markdown(issue, ref_map):
|
||||
ref = ref_map[issue["number"]]
|
||||
return f"[{ref}]({issue['url']})"
|
||||
|
||||
|
||||
def summary_inputs(issues, limit=80, ref_map=None):
|
||||
ranked = ranked_digest_issues(issues)
|
||||
if ref_map is None:
|
||||
ref_map = {issue["number"]: ref for ref, issue in enumerate(ranked, start=1)}
|
||||
rows = []
|
||||
for issue in ranked[:limit]:
|
||||
rows.append(
|
||||
{
|
||||
"ref": ref_map[issue["number"]],
|
||||
"ref_markdown": issue_ref_markdown(issue, ref_map),
|
||||
"number": issue["number"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
"url": issue["url"],
|
||||
"labels": issue["labels"],
|
||||
"owner_labels": issue["owner_labels"],
|
||||
"kind_labels": issue["kind_labels"],
|
||||
"state": issue.get("state", ""),
|
||||
"attention_marker": issue.get("attention_marker", ""),
|
||||
"interactions": issue["user_interactions"],
|
||||
"new_comments": issue["activity"].get("new_comments", 0),
|
||||
"new_reactions": issue.get("new_reactions", 0),
|
||||
"new_upvotes": issue.get("new_upvotes", 0),
|
||||
"current_reactions": issue.get("issue_reaction_total", 0)
|
||||
+ issue.get("comment_reaction_total", 0),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def collect_digest(args):
|
||||
since, until = resolve_window(args)
|
||||
window_hours = (until - since).total_seconds() / 3600
|
||||
attention_thresholds = attention_thresholds_for_window(window_hours)
|
||||
requested_labels, all_labels = normalize_requested_labels(
|
||||
args.labels, all_labels=args.all_labels
|
||||
)
|
||||
queries = build_search_queries(
|
||||
args.repo, requested_labels, since, all_labels=all_labels
|
||||
)
|
||||
numbers = search_issue_numbers(queries, args.limit_issues)
|
||||
gh_version_output = gh_text(["--version"])
|
||||
|
||||
issues = []
|
||||
max_comment_pages = None if args.max_comment_pages <= 0 else args.max_comment_pages
|
||||
for number in numbers:
|
||||
issue = fetch_issue(args.repo, number)
|
||||
comments_since = None if args.fetch_all_comments else since
|
||||
comments_payload = fetch_comments(
|
||||
args.repo,
|
||||
number,
|
||||
since=comments_since,
|
||||
max_pages=max_comment_pages,
|
||||
)
|
||||
comments = comments_payload["items"]
|
||||
issue_reaction_events = fetch_reactions_for_item(
|
||||
f"repos/{args.repo}/issues/{number}/reactions", issue
|
||||
)
|
||||
comment_reactions_by_id = fetch_comment_reactions(args.repo, comments)
|
||||
comments_hydration = {
|
||||
"fetched": len(comments),
|
||||
"total": int(issue.get("comments") or len(comments) or 0),
|
||||
"since": format_timestamp(comments_since) if comments_since else None,
|
||||
"truncated": comments_payload["truncated"],
|
||||
"max_pages": comments_payload["max_pages"],
|
||||
"fetch_all_comments": args.fetch_all_comments,
|
||||
}
|
||||
summary = summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
requested_labels,
|
||||
since,
|
||||
until,
|
||||
args.body_chars,
|
||||
args.comment_chars,
|
||||
issue_reaction_events=issue_reaction_events,
|
||||
comment_reactions_by_id=comment_reactions_by_id,
|
||||
all_labels=all_labels,
|
||||
comments_hydration=comments_hydration,
|
||||
attention_thresholds=attention_thresholds,
|
||||
)
|
||||
if summary is not None:
|
||||
issues.append(summary)
|
||||
|
||||
issues.sort(
|
||||
key=lambda issue: (issue["updated_at"], int(issue["number"] or 0)), reverse=True
|
||||
)
|
||||
totals = {
|
||||
"candidate_issues": len(numbers),
|
||||
"included_issues": len(issues),
|
||||
"new_issues": sum(1 for issue in issues if issue["activity"]["new_issue"]),
|
||||
"issues_with_new_comments": sum(
|
||||
1 for issue in issues if issue["activity"]["new_comments"] > 0
|
||||
),
|
||||
"new_comments": sum(issue["activity"]["new_comments"] for issue in issues),
|
||||
"comments_fetched": sum(
|
||||
issue["comments_hydration"]["fetched"] for issue in issues
|
||||
),
|
||||
"issues_with_truncated_comment_hydration": sum(
|
||||
1 for issue in issues if issue["comments_hydration"]["truncated"]
|
||||
),
|
||||
"updated_without_visible_new_post": sum(
|
||||
1
|
||||
for issue in issues
|
||||
if issue["activity"]["updated_without_visible_new_post"]
|
||||
),
|
||||
"issue_reactions_current_total": sum(
|
||||
issue["issue_reaction_total"] for issue in issues
|
||||
),
|
||||
"comment_reactions_current_total": sum(
|
||||
issue["comment_reaction_total"] for issue in issues
|
||||
),
|
||||
"new_reactions": sum(issue["new_reactions"] for issue in issues),
|
||||
"new_upvotes": sum(issue["new_upvotes"] for issue in issues),
|
||||
"user_interactions": sum(issue["user_interactions"] for issue in issues),
|
||||
}
|
||||
ranked = ranked_digest_issues(issues)
|
||||
ref_map = {issue["number"]: ref for ref, issue in enumerate(ranked, start=1)}
|
||||
filter_label = "all" if all_labels else requested_labels
|
||||
|
||||
return {
|
||||
"generated_at": format_timestamp(datetime.now(timezone.utc)),
|
||||
"source": {
|
||||
"repo": args.repo,
|
||||
"skill": "codex-issue-digest",
|
||||
"collector": skill_relative_path(),
|
||||
"script_version": SCRIPT_VERSION,
|
||||
"git_head": git_head(),
|
||||
"gh_version": gh_version_output.splitlines()[0]
|
||||
if gh_version_output
|
||||
else None,
|
||||
},
|
||||
"window": {
|
||||
"since": format_timestamp(since),
|
||||
"until": format_timestamp(until),
|
||||
"hours": round(window_hours, 3),
|
||||
},
|
||||
"attention_thresholds": attention_thresholds,
|
||||
"filters": {
|
||||
"owner_labels": filter_label,
|
||||
"all_labels": all_labels,
|
||||
"kind_labels": list(QUALIFYING_KIND_LABELS),
|
||||
},
|
||||
"collection_notes": [
|
||||
"Issues are selected when they currently have bug or enhancement plus at least one requested owner label and were updated during the window.",
|
||||
"By default, issue comments are fetched with since=window_start and a max page cap to avoid long historical threads; use --fetch-all-comments when exhaustive comment history is needed.",
|
||||
"New issue comments are filtered by comment creation time within the window from the fetched comment set.",
|
||||
"Reaction events are counted by GitHub reaction created_at timestamps for hydrated issues and fetched comments.",
|
||||
"Current reaction totals are standing engagement signals; new_reactions and new_upvotes are windowed activity.",
|
||||
"The collector does not assign semantic clusters; use summary_inputs as model-ready evidence for report-time clustering.",
|
||||
"Pure reaction-only issues may be missed if GitHub issue search does not surface them via updated_at.",
|
||||
"Issues updated during the window without a new issue body or new comment are retained because label/status edits can still be useful owner signals.",
|
||||
],
|
||||
"totals": totals,
|
||||
"by_owner_label": count_by_label(
|
||||
issues,
|
||||
sorted(
|
||||
{area for issue in issues for area in issue["owner_labels"]},
|
||||
key=str.casefold,
|
||||
)
|
||||
if all_labels
|
||||
else requested_labels,
|
||||
),
|
||||
"by_kind_label": count_by_kind(issues),
|
||||
"hot_items": hot_items(issues),
|
||||
"summary_inputs": summary_inputs(issues, ref_map=ref_map),
|
||||
"digest_rows": digest_rows(issues, ref_map=ref_map),
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
try:
|
||||
digest = collect_digest(args)
|
||||
except (GhCommandError, RuntimeError, ValueError) as err:
|
||||
sys.stderr.write(f"collect_issue_digest.py error: {err}\n")
|
||||
return 1
|
||||
sys.stdout.write(json.dumps(digest, indent=2, sort_keys=True) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,614 @@
|
||||
import importlib.util
|
||||
from datetime import timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).with_name("collect_issue_digest.py")
|
||||
MODULE_SPEC = importlib.util.spec_from_file_location(
|
||||
"collect_issue_digest", MODULE_PATH
|
||||
)
|
||||
collect_issue_digest = importlib.util.module_from_spec(MODULE_SPEC)
|
||||
assert MODULE_SPEC.loader is not None
|
||||
MODULE_SPEC.loader.exec_module(collect_issue_digest)
|
||||
|
||||
|
||||
def test_build_search_queries_uses_each_owner_and_kind_label():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T12:34:56Z", "--since")
|
||||
|
||||
queries = collect_issue_digest.build_search_queries(
|
||||
"openai/codex", ["tui", "exec"], since
|
||||
)
|
||||
|
||||
assert queries == [
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:tui label:bug",
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:tui label:enhancement",
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:exec label:bug",
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:exec label:enhancement",
|
||||
]
|
||||
|
||||
|
||||
def test_build_search_queries_can_scan_all_labels():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T12:34:56Z", "--since")
|
||||
|
||||
queries = collect_issue_digest.build_search_queries(
|
||||
"openai/codex", [], since, all_labels=True
|
||||
)
|
||||
|
||||
assert queries == [
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:bug",
|
||||
"repo:openai/codex is:issue updated:>=2026-04-25 label:enhancement",
|
||||
]
|
||||
|
||||
|
||||
def test_normalize_requested_labels_accepts_all_area_phrases():
|
||||
assert collect_issue_digest.normalize_requested_labels(["all", "areas"]) == (
|
||||
[],
|
||||
True,
|
||||
)
|
||||
assert collect_issue_digest.normalize_requested_labels(["all-labels"]) == (
|
||||
[],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def test_summarize_issue_keeps_new_comments_and_reaction_signals():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T00:00:00Z", "--since")
|
||||
until = collect_issue_digest.parse_timestamp("2026-04-26T00:00:00Z", "--until")
|
||||
issue = {
|
||||
"number": 123,
|
||||
"title": "TUI does not redraw",
|
||||
"html_url": "https://github.com/openai/codex/issues/123",
|
||||
"state": "open",
|
||||
"created_at": "2026-04-24T20:00:00Z",
|
||||
"updated_at": "2026-04-25T10:00:00Z",
|
||||
"user": {"login": "alice"},
|
||||
"author_association": "NONE",
|
||||
"comments": 2,
|
||||
"body": "The terminal freezes after resize.",
|
||||
"labels": [{"name": "bug"}, {"name": "tui"}],
|
||||
"reactions": {"total_count": 3, "+1": 2, "rocket": 1},
|
||||
}
|
||||
comments = [
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": "2026-04-25T11:00:00Z",
|
||||
"updated_at": "2026-04-25T11:00:00Z",
|
||||
"html_url": "https://github.com/openai/codex/issues/123#issuecomment-1",
|
||||
"user": {"login": "bob"},
|
||||
"author_association": "MEMBER",
|
||||
"body": "I can reproduce this on main.",
|
||||
"reactions": {"total_count": 4, "heart": 1, "+1": 3},
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"created_at": "2026-04-24T11:00:00Z",
|
||||
"updated_at": "2026-04-24T11:00:00Z",
|
||||
"html_url": "https://github.com/openai/codex/issues/123#issuecomment-2",
|
||||
"user": {"login": "carol"},
|
||||
"author_association": "NONE",
|
||||
"body": "Older comment.",
|
||||
"reactions": {"total_count": 1, "eyes": 1},
|
||||
},
|
||||
]
|
||||
|
||||
summary = collect_issue_digest.summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
["tui", "exec"],
|
||||
since,
|
||||
until,
|
||||
body_chars=200,
|
||||
comment_chars=200,
|
||||
)
|
||||
|
||||
assert summary == {
|
||||
"number": 123,
|
||||
"title": "TUI does not redraw",
|
||||
"description": "TUI does not redraw",
|
||||
"url": "https://github.com/openai/codex/issues/123",
|
||||
"state": "open",
|
||||
"author": "alice",
|
||||
"author_association": "NONE",
|
||||
"created_at": "2026-04-24T20:00:00Z",
|
||||
"updated_at": "2026-04-25T10:00:00Z",
|
||||
"labels": ["bug", "tui"],
|
||||
"kind_labels": ["bug"],
|
||||
"owner_labels": ["tui"],
|
||||
"comments_total": 2,
|
||||
"comments_hydration": {
|
||||
"fetched": 2,
|
||||
"since": None,
|
||||
"truncated": False,
|
||||
"max_pages": None,
|
||||
},
|
||||
"issue_reactions": {"+1": 2, "rocket": 1},
|
||||
"issue_reaction_total": 3,
|
||||
"comment_reaction_total": 5,
|
||||
"new_comment_reaction_total": 4,
|
||||
"new_issue_reactions": 0,
|
||||
"new_issue_upvotes": 0,
|
||||
"new_comment_reactions": 0,
|
||||
"new_comment_upvotes": 0,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"user_interactions": 1,
|
||||
"attention": False,
|
||||
"attention_level": 0,
|
||||
"attention_marker": "",
|
||||
"engagement_score": 12,
|
||||
"activity": {
|
||||
"new_issue": False,
|
||||
"new_comments": 1,
|
||||
"new_human_comments": 1,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"updated_without_visible_new_post": False,
|
||||
},
|
||||
"body_excerpt": "The terminal freezes after resize.",
|
||||
"new_comments": [
|
||||
{
|
||||
"id": 1,
|
||||
"author": "bob",
|
||||
"author_association": "MEMBER",
|
||||
"created_at": "2026-04-25T11:00:00Z",
|
||||
"updated_at": "2026-04-25T11:00:00Z",
|
||||
"url": "https://github.com/openai/codex/issues/123#issuecomment-1",
|
||||
"human_user_interaction": True,
|
||||
"reactions": {"+1": 3, "heart": 1},
|
||||
"reaction_total": 4,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"new_reaction_counts": {},
|
||||
"body_excerpt": "I can reproduce this on main.",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_summarize_issue_filters_non_owner_or_non_kind_labels():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T00:00:00Z", "--since")
|
||||
until = collect_issue_digest.parse_timestamp("2026-04-26T00:00:00Z", "--until")
|
||||
base_issue = {
|
||||
"number": 1,
|
||||
"title": "Question",
|
||||
"created_at": "2026-04-25T01:00:00Z",
|
||||
"updated_at": "2026-04-25T01:00:00Z",
|
||||
"labels": [{"name": "question"}, {"name": "tui"}],
|
||||
}
|
||||
|
||||
assert (
|
||||
collect_issue_digest.summarize_issue(
|
||||
base_issue,
|
||||
[],
|
||||
["tui"],
|
||||
since,
|
||||
until,
|
||||
body_chars=100,
|
||||
comment_chars=100,
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
issue_without_owner = dict(base_issue)
|
||||
issue_without_owner["labels"] = [{"name": "bug"}, {"name": "app"}]
|
||||
|
||||
assert (
|
||||
collect_issue_digest.summarize_issue(
|
||||
issue_without_owner,
|
||||
[],
|
||||
["tui"],
|
||||
since,
|
||||
until,
|
||||
body_chars=100,
|
||||
comment_chars=100,
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_window_defaults_to_previous_hours():
|
||||
class Args:
|
||||
since = None
|
||||
until = "2026-04-26T12:00:00Z"
|
||||
window_hours = 24
|
||||
|
||||
since, until = collect_issue_digest.resolve_window(Args())
|
||||
|
||||
assert since.isoformat() == "2026-04-25T12:00:00+00:00"
|
||||
assert until.tzinfo == timezone.utc
|
||||
|
||||
|
||||
def test_parse_duration_hours_accepts_common_phrases():
|
||||
assert collect_issue_digest.parse_duration_hours("past week") == 168
|
||||
assert collect_issue_digest.parse_duration_hours("48h") == 48
|
||||
assert collect_issue_digest.parse_duration_hours("2 days") == 48
|
||||
assert collect_issue_digest.parse_duration_hours("1w") == 168
|
||||
|
||||
|
||||
def test_attention_thresholds_scale_by_window_length():
|
||||
one_day = collect_issue_digest.attention_thresholds_for_window(24)
|
||||
assert one_day["elevated"] == 10
|
||||
assert one_day["very_high"] == 20
|
||||
|
||||
half_day = collect_issue_digest.attention_thresholds_for_window(12)
|
||||
assert half_day["elevated"] == 5
|
||||
assert half_day["very_high"] == 10
|
||||
|
||||
week = collect_issue_digest.attention_thresholds_for_window(168)
|
||||
assert week["elevated"] == 70
|
||||
assert week["very_high"] == 140
|
||||
assert collect_issue_digest.attention_marker_for(69, week) == ""
|
||||
assert collect_issue_digest.attention_marker_for(107, week) == "🔥"
|
||||
assert collect_issue_digest.attention_marker_for(140, week) == "🔥🔥"
|
||||
|
||||
|
||||
def test_fetch_comments_uses_since_filter_and_page_cap(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_gh_json(args):
|
||||
calls.append(args)
|
||||
return [{"id": idx} for idx in range(100)]
|
||||
|
||||
monkeypatch.setattr(collect_issue_digest, "gh_json", fake_gh_json)
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T00:00:00Z", "--since")
|
||||
|
||||
payload = collect_issue_digest.fetch_comments(
|
||||
"openai/codex", 123, since=since, max_pages=1
|
||||
)
|
||||
|
||||
assert len(payload["items"]) == 100
|
||||
assert payload["truncated"] is True
|
||||
assert payload["max_pages"] == 1
|
||||
assert calls == [
|
||||
[
|
||||
"api",
|
||||
"repos/openai/codex/issues/123/comments?since=2026-04-25T00%3A00%3A00Z&per_page=100&page=1",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def test_issue_description_prefers_title_over_body_noise():
|
||||
issue = {
|
||||
"title": "Codex.app GUI: MCP child processes not reaped after task completion",
|
||||
"body": "A later crash mention should not override the title-level symptom.",
|
||||
"labels": [{"name": "app"}, {"name": "bug"}],
|
||||
}
|
||||
|
||||
description = collect_issue_digest.issue_description(issue)
|
||||
assert "MCP child processes" in description
|
||||
assert "crash" not in description.casefold()
|
||||
|
||||
|
||||
def test_attention_markers_count_human_user_interactions():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T00:00:00Z", "--since")
|
||||
until = collect_issue_digest.parse_timestamp("2026-04-26T00:00:00Z", "--until")
|
||||
issue = {
|
||||
"number": 456,
|
||||
"title": "Agent context is exploding",
|
||||
"html_url": "https://github.com/openai/codex/issues/456",
|
||||
"state": "open",
|
||||
"created_at": "2026-04-25T01:00:00Z",
|
||||
"updated_at": "2026-04-25T12:00:00Z",
|
||||
"user": {"login": "alice"},
|
||||
"labels": [{"name": "bug"}, {"name": "agent"}],
|
||||
}
|
||||
comments = [
|
||||
{
|
||||
"id": idx,
|
||||
"created_at": "2026-04-25T02:00:00Z",
|
||||
"updated_at": "2026-04-25T02:00:00Z",
|
||||
"user": {"login": f"user-{idx}"},
|
||||
"body": "same here",
|
||||
}
|
||||
for idx in range(9)
|
||||
]
|
||||
comments.append(
|
||||
{
|
||||
"id": 99,
|
||||
"created_at": "2026-04-25T02:00:00Z",
|
||||
"updated_at": "2026-04-25T02:00:00Z",
|
||||
"user": {"login": "github-actions[bot]"},
|
||||
"body": "duplicate bot note",
|
||||
}
|
||||
)
|
||||
|
||||
summary = collect_issue_digest.summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
["agent"],
|
||||
since,
|
||||
until,
|
||||
body_chars=100,
|
||||
comment_chars=100,
|
||||
)
|
||||
|
||||
assert summary["user_interactions"] == 10
|
||||
assert summary["activity"]["new_human_comments"] == 9
|
||||
assert summary["attention"] is True
|
||||
assert summary["attention_level"] == 1
|
||||
assert summary["attention_marker"] == "🔥"
|
||||
|
||||
issue["created_at"] = "2026-04-24T01:00:00Z"
|
||||
comments.extend(
|
||||
{
|
||||
"id": idx,
|
||||
"created_at": "2026-04-25T03:00:00Z",
|
||||
"updated_at": "2026-04-25T03:00:00Z",
|
||||
"user": {"login": f"extra-user-{idx}"},
|
||||
"body": "also seeing this",
|
||||
}
|
||||
for idx in range(11)
|
||||
)
|
||||
|
||||
summary = collect_issue_digest.summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
["agent"],
|
||||
since,
|
||||
until,
|
||||
body_chars=100,
|
||||
comment_chars=100,
|
||||
)
|
||||
|
||||
assert summary["user_interactions"] == 20
|
||||
assert summary["attention_level"] == 2
|
||||
assert summary["attention_marker"] == "🔥🔥"
|
||||
|
||||
|
||||
def test_reactions_count_toward_attention_markers():
|
||||
since = collect_issue_digest.parse_timestamp("2026-04-25T00:00:00Z", "--since")
|
||||
until = collect_issue_digest.parse_timestamp("2026-04-26T00:00:00Z", "--until")
|
||||
issue = {
|
||||
"number": 789,
|
||||
"title": "Support 1M token context",
|
||||
"html_url": "https://github.com/openai/codex/issues/789",
|
||||
"state": "open",
|
||||
"created_at": "2026-04-24T01:00:00Z",
|
||||
"updated_at": "2026-04-25T12:00:00Z",
|
||||
"user": {"login": "alice"},
|
||||
"labels": [{"name": "enhancement"}, {"name": "context"}],
|
||||
"reactions": {"total_count": 20, "+1": 20},
|
||||
}
|
||||
comments = [
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": "2026-04-25T02:00:00Z",
|
||||
"updated_at": "2026-04-25T02:00:00Z",
|
||||
"user": {"login": "commenter"},
|
||||
"body": "please",
|
||||
"reactions": {"total_count": 2, "+1": 2},
|
||||
}
|
||||
]
|
||||
issue_reactions = [
|
||||
{
|
||||
"content": "+1",
|
||||
"created_at": "2026-04-25T03:00:00Z",
|
||||
"user": {"login": f"reactor-{idx}"},
|
||||
}
|
||||
for idx in range(18)
|
||||
]
|
||||
comment_reactions_by_id = {
|
||||
1: [
|
||||
{
|
||||
"content": "heart",
|
||||
"created_at": "2026-04-25T04:00:00Z",
|
||||
"user": {"login": "human-reactor"},
|
||||
},
|
||||
{
|
||||
"content": "+1",
|
||||
"created_at": "2026-04-25T04:00:00Z",
|
||||
"user": {"login": "github-actions[bot]"},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
summary = collect_issue_digest.summarize_issue(
|
||||
issue,
|
||||
comments,
|
||||
["context"],
|
||||
since,
|
||||
until,
|
||||
body_chars=100,
|
||||
comment_chars=100,
|
||||
issue_reaction_events=issue_reactions,
|
||||
comment_reactions_by_id=comment_reactions_by_id,
|
||||
)
|
||||
|
||||
assert summary["new_reactions"] == 19
|
||||
assert summary["new_upvotes"] == 18
|
||||
assert summary["user_interactions"] == 20
|
||||
assert summary["attention_level"] == 2
|
||||
assert summary["attention_marker"] == "🔥🔥"
|
||||
assert summary["new_comments"][0]["new_reactions"] == 1
|
||||
assert summary["new_comments"][0]["new_upvotes"] == 0
|
||||
|
||||
|
||||
def test_digest_rows_are_table_ready_with_concise_descriptions():
|
||||
rows = collect_issue_digest.digest_rows(
|
||||
[
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiet bug",
|
||||
"description": "Quiet bug",
|
||||
"url": "https://github.com/openai/codex/issues/1",
|
||||
"owner_labels": ["context"],
|
||||
"kind_labels": ["bug"],
|
||||
"state": "open",
|
||||
"attention": False,
|
||||
"attention_level": 0,
|
||||
"attention_marker": "",
|
||||
"user_interactions": 1,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"engagement_score": 3,
|
||||
"issue_reaction_total": 0,
|
||||
"comment_reaction_total": 0,
|
||||
"updated_at": "2026-04-25T01:00:00Z",
|
||||
"activity": {
|
||||
"new_issue": True,
|
||||
"new_comments": 0,
|
||||
"new_reactions": 0,
|
||||
"updated_without_visible_new_post": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Busy bug",
|
||||
"description": "High-volume bug report",
|
||||
"url": "https://github.com/openai/codex/issues/2",
|
||||
"owner_labels": ["agent"],
|
||||
"kind_labels": ["bug"],
|
||||
"state": "open",
|
||||
"attention": True,
|
||||
"attention_level": 1,
|
||||
"attention_marker": "🔥",
|
||||
"user_interactions": 17,
|
||||
"new_reactions": 3,
|
||||
"new_upvotes": 2,
|
||||
"engagement_score": 20,
|
||||
"issue_reaction_total": 5,
|
||||
"comment_reaction_total": 2,
|
||||
"updated_at": "2026-04-25T02:00:00Z",
|
||||
"activity": {
|
||||
"new_issue": False,
|
||||
"new_comments": 16,
|
||||
"new_reactions": 3,
|
||||
"updated_without_visible_new_post": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
assert rows[0] == {
|
||||
"ref": 1,
|
||||
"ref_markdown": "[1](https://github.com/openai/codex/issues/2)",
|
||||
"marker": "🔥",
|
||||
"attention_marker": "🔥",
|
||||
"number": 2,
|
||||
"description": "High-volume bug report",
|
||||
"title": "Busy bug",
|
||||
"url": "https://github.com/openai/codex/issues/2",
|
||||
"area": "agent",
|
||||
"kind": "bug",
|
||||
"state": "open",
|
||||
"interactions": 17,
|
||||
"user_interactions": 17,
|
||||
"new_reactions": 3,
|
||||
"new_upvotes": 2,
|
||||
"current_reactions": 7,
|
||||
}
|
||||
|
||||
|
||||
def test_summary_inputs_are_model_ready_without_preclustering():
|
||||
issues = [
|
||||
{
|
||||
"number": 20,
|
||||
"title": "Windows app Browser Use external navigation fails",
|
||||
"description": "Browser Use navigation or app-server failure",
|
||||
"url": "https://github.com/openai/codex/issues/20",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"attention": False,
|
||||
"attention_level": 0,
|
||||
"attention_marker": "",
|
||||
"user_interactions": 3,
|
||||
"new_reactions": 1,
|
||||
"engagement_score": 8,
|
||||
"updated_at": "2026-04-25T04:00:00Z",
|
||||
"activity": {"new_comments": 2},
|
||||
},
|
||||
{
|
||||
"number": 21,
|
||||
"title": "On Windows, cmake output waits until timeout",
|
||||
"description": "Windows command timeout/capture problem",
|
||||
"url": "https://github.com/openai/codex/issues/21",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"attention": False,
|
||||
"attention_level": 0,
|
||||
"attention_marker": "",
|
||||
"user_interactions": 3,
|
||||
"new_reactions": 0,
|
||||
"engagement_score": 7,
|
||||
"updated_at": "2026-04-25T03:00:00Z",
|
||||
"activity": {"new_comments": 3},
|
||||
},
|
||||
{
|
||||
"number": 22,
|
||||
"title": "Windows computer use tool fails to click buttons",
|
||||
"description": "Computer-use workflow failure",
|
||||
"url": "https://github.com/openai/codex/issues/22",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"attention": False,
|
||||
"attention_level": 0,
|
||||
"attention_marker": "",
|
||||
"user_interactions": 3,
|
||||
"new_reactions": 0,
|
||||
"engagement_score": 6,
|
||||
"updated_at": "2026-04-25T02:00:00Z",
|
||||
"activity": {"new_comments": 3},
|
||||
},
|
||||
]
|
||||
|
||||
rows = collect_issue_digest.summary_inputs(issues, ref_map={20: 1, 21: 2, 22: 3})
|
||||
|
||||
assert rows == [
|
||||
{
|
||||
"ref": 1,
|
||||
"ref_markdown": "[1](https://github.com/openai/codex/issues/20)",
|
||||
"number": 20,
|
||||
"title": "Windows app Browser Use external navigation fails",
|
||||
"description": "Browser Use navigation or app-server failure",
|
||||
"url": "https://github.com/openai/codex/issues/20",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"state": "",
|
||||
"attention_marker": "",
|
||||
"interactions": 3,
|
||||
"new_comments": 2,
|
||||
"new_reactions": 1,
|
||||
"new_upvotes": 0,
|
||||
"current_reactions": 0,
|
||||
},
|
||||
{
|
||||
"ref": 2,
|
||||
"ref_markdown": "[2](https://github.com/openai/codex/issues/21)",
|
||||
"number": 21,
|
||||
"title": "On Windows, cmake output waits until timeout",
|
||||
"description": "Windows command timeout/capture problem",
|
||||
"url": "https://github.com/openai/codex/issues/21",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"state": "",
|
||||
"attention_marker": "",
|
||||
"interactions": 3,
|
||||
"new_comments": 3,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"current_reactions": 0,
|
||||
},
|
||||
{
|
||||
"ref": 3,
|
||||
"ref_markdown": "[3](https://github.com/openai/codex/issues/22)",
|
||||
"number": 22,
|
||||
"title": "Windows computer use tool fails to click buttons",
|
||||
"description": "Computer-use workflow failure",
|
||||
"url": "https://github.com/openai/codex/issues/22",
|
||||
"labels": ["app", "bug"],
|
||||
"owner_labels": ["app"],
|
||||
"kind_labels": ["bug"],
|
||||
"state": "",
|
||||
"attention_marker": "",
|
||||
"interactions": 3,
|
||||
"new_comments": 3,
|
||||
"new_reactions": 0,
|
||||
"new_upvotes": 0,
|
||||
"current_reactions": 0,
|
||||
},
|
||||
]
|
||||
@@ -4,9 +4,11 @@ ARG TZ
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG NODE_MAJOR=22
|
||||
ARG RUST_TOOLCHAIN=1.92.0
|
||||
ARG CODEX_NPM_VERSION=latest
|
||||
# Keep this in sync with .devcontainer/codex-install/package.json and pnpm-lock.yaml.
|
||||
ARG CODEX_NPM_VERSION=0.121.0
|
||||
|
||||
ENV TZ="$TZ"
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
@@ -43,12 +45,18 @@ RUN apt-get update \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY .devcontainer/codex-install/package.json \
|
||||
.devcontainer/codex-install/pnpm-lock.yaml \
|
||||
.devcontainer/codex-install/pnpm-workspace.yaml \
|
||||
/opt/codex-install/
|
||||
|
||||
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& npm install -g corepack@latest "@openai/codex@${CODEX_NPM_VERSION}" \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@10.28.2 --activate \
|
||||
&& test "$(node -p "require('/opt/codex-install/package.json').dependencies['@openai/codex']")" = "${CODEX_NPM_VERSION}" \
|
||||
&& cd /opt/codex-install \
|
||||
&& corepack pnpm install --prod --frozen-lockfile \
|
||||
&& ln -s /opt/codex-install/node_modules/.bin/codex /usr/local/bin/codex \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "codex-devcontainer-install",
|
||||
"private": true,
|
||||
"description": "Locked Codex CLI install boundary for the secure devcontainer.",
|
||||
"dependencies": {
|
||||
"@openai/codex": "0.121.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"pnpm": ">=10.33.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@openai/codex':
|
||||
specifier: 0.121.0
|
||||
version: 0.121.0
|
||||
|
||||
packages:
|
||||
|
||||
'@openai/codex@0.121.0':
|
||||
resolution: {integrity: sha512-kCJ2NeATd4QBQRmqV04ymdN1ZU3MSwnJQDm/KzjpuzGvCuUVEn7no/T2mRyxQ2x77AACqriNOyPPoM/yufyvNg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
'@openai/codex@0.121.0-darwin-arm64':
|
||||
resolution: {integrity: sha512-ZyBqIB6Fb4I0hGb/h65Vu7ePYjHSmGiqqfm+/1djEuxDPkqjfi4wkxYxNYNY+6najyNGN4UijOSTTf19eDCrqw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.121.0-darwin-x64':
|
||||
resolution: {integrity: sha512-1/OAtdkAZ5yPI3xqaEFlHuPziS1yCqL2gOZdswE7HTmmwpIxi6Z3FCo60JWDPluIp89z4tftdjq73/OCN0YVcw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.121.0-linux-arm64':
|
||||
resolution: {integrity: sha512-2UgMmdo237o7SCMsfb529cOSEM2HFUgN6OBkv5SBLwfNY1NO2Ex6JnUjlppEXlX6/4cXfZ5qjDghVz5j/+B9zw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.121.0-linux-x64':
|
||||
resolution: {integrity: sha512-vlpNJXIqss800J+32Vy7TUZzv31n61b45OLxmsVQGFkTNLJcjFrj9jDUC7I62eC4F16gLioilefNfv4CdJQOEw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.121.0-win32-arm64':
|
||||
resolution: {integrity: sha512-m88q4f3XI5npn1t6OG0nWGHWWAjO5FgjRwxh4hdujbLO6t9CiCNfhfPZIOSsoATbrCNwLC+6S77m3cjbNToPNg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@openai/codex@0.121.0-win32-x64':
|
||||
resolution: {integrity: sha512-Fp0ecVOyM+VcBi/y4HVvRzhifO9YqRiHzhV3rhtAppC7flh22WPguLC4kmvXYAR0p3RPzbo35M2CedWnkOT+cw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
snapshots:
|
||||
|
||||
'@openai/codex@0.121.0':
|
||||
optionalDependencies:
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.121.0-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.121.0-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.121.0-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.121.0-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.121.0-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.121.0-win32-x64'
|
||||
|
||||
'@openai/codex@0.121.0-darwin-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.121.0-darwin-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.121.0-linux-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.121.0-linux-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.121.0-win32-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.121.0-win32-x64':
|
||||
optional: true
|
||||
@@ -0,0 +1,12 @@
|
||||
packages:
|
||||
- "."
|
||||
|
||||
minimumReleaseAge: 10080
|
||||
minimumReleaseAgeExclude: []
|
||||
|
||||
blockExoticSubdeps: true
|
||||
strictDepBuilds: true
|
||||
trustPolicy: no-downgrade
|
||||
trustPolicyIgnoreAfter: 10080
|
||||
trustPolicyExclude: []
|
||||
allowBuilds: {}
|
||||
@@ -8,7 +8,7 @@
|
||||
"TZ": "${localEnv:TZ:UTC}",
|
||||
"NODE_MAJOR": "22",
|
||||
"RUST_TOOLCHAIN": "1.92.0",
|
||||
"CODEX_NPM_VERSION": "latest"
|
||||
"CODEX_NPM_VERSION": "0.121.0"
|
||||
}
|
||||
},
|
||||
"runArgs": [
|
||||
|
||||
@@ -7,6 +7,9 @@ inputs:
|
||||
artifacts-dir:
|
||||
description: Absolute path to the directory containing built binaries to sign.
|
||||
required: true
|
||||
binaries:
|
||||
description: Space-delimited binary basenames to sign.
|
||||
default: "codex codex-responses-api-proxy"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -18,6 +21,7 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
ARTIFACTS_DIR: ${{ inputs.artifacts-dir }}
|
||||
BINARIES: ${{ inputs.binaries }}
|
||||
COSIGN_EXPERIMENTAL: "1"
|
||||
COSIGN_YES: "true"
|
||||
COSIGN_OIDC_CLIENT_ID: "sigstore"
|
||||
@@ -31,7 +35,7 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for binary in codex codex-responses-api-proxy; do
|
||||
for binary in ${BINARIES}; do
|
||||
artifact="${dest}/${binary}"
|
||||
if [[ ! -f "$artifact" ]]; then
|
||||
echo "Binary $artifact not found"
|
||||
|
||||
@@ -4,6 +4,9 @@ inputs:
|
||||
target:
|
||||
description: Rust compilation target triple (e.g. aarch64-apple-darwin).
|
||||
required: true
|
||||
binaries:
|
||||
description: Space-delimited binary basenames to sign and notarize.
|
||||
default: "codex codex-responses-api-proxy"
|
||||
sign-binaries:
|
||||
description: Whether to sign and notarize the macOS binaries.
|
||||
required: false
|
||||
@@ -119,6 +122,7 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
BINARIES: ${{ inputs.binaries }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -134,7 +138,7 @@ runs:
|
||||
|
||||
entitlements_path="$GITHUB_ACTION_PATH/codex.entitlements.plist"
|
||||
|
||||
for binary in codex codex-responses-api-proxy; do
|
||||
for binary in ${BINARIES}; do
|
||||
path="codex-rs/target/${TARGET}/release/${binary}"
|
||||
codesign --force --options runtime --timestamp --entitlements "$entitlements_path" --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||||
done
|
||||
@@ -144,6 +148,7 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
BINARIES: ${{ inputs.binaries }}
|
||||
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
|
||||
@@ -182,8 +187,9 @@ runs:
|
||||
notarize_submission "$binary" "$archive_path" "$notary_key_path"
|
||||
}
|
||||
|
||||
notarize_binary "codex"
|
||||
notarize_binary "codex-responses-api-proxy"
|
||||
for binary in ${BINARIES}; do
|
||||
notarize_binary "${binary}"
|
||||
done
|
||||
|
||||
- name: Sign and notarize macOS dmg
|
||||
if: ${{ inputs.sign-dmg == 'true' }}
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>2DC432GLL2.com.openai.codex</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>2DC432GLL2</string>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>2DC432GLL2.com.openai.codex</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -8,7 +8,7 @@ inputs:
|
||||
description: Logical namespace used to keep concurrent Bazel jobs from reserving the same repository cache key.
|
||||
required: true
|
||||
install-test-prereqs:
|
||||
description: Install Node.js and DotSlash for Bazel-backed test jobs.
|
||||
description: Install DotSlash for Bazel-backed test jobs.
|
||||
required: false
|
||||
default: "false"
|
||||
outputs:
|
||||
|
||||
@@ -5,7 +5,7 @@ inputs:
|
||||
description: Target triple used for cache namespacing.
|
||||
required: true
|
||||
install-test-prereqs:
|
||||
description: Install Node.js and DotSlash for Bazel-backed test jobs.
|
||||
description: Install DotSlash for Bazel-backed test jobs.
|
||||
required: false
|
||||
default: "false"
|
||||
outputs:
|
||||
@@ -16,12 +16,6 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Node.js for js_repl tests
|
||||
if: inputs.install-test-prereqs == 'true'
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: codex-rs/node-version.txt
|
||||
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
@@ -122,6 +116,11 @@ runs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Compute cache-stable Windows Bazel PATH
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/compute-bazel-windows-path.ps1
|
||||
|
||||
- name: Enable Git long paths (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
|
||||
@@ -4,6 +4,9 @@ inputs:
|
||||
target:
|
||||
description: Target triple for the artifacts to sign.
|
||||
required: true
|
||||
binaries:
|
||||
description: Space-delimited binary basenames to sign.
|
||||
default: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner"
|
||||
client-id:
|
||||
description: Azure Trusted Signing client ID.
|
||||
required: true
|
||||
@@ -33,6 +36,23 @@ runs:
|
||||
tenant-id: ${{ inputs.tenant-id }}
|
||||
subscription-id: ${{ inputs.subscription-id }}
|
||||
|
||||
- name: Prepare file list
|
||||
id: prepare
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
BINARIES: ${{ inputs.binaries }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
{
|
||||
echo "files<<EOF"
|
||||
for binary in ${BINARIES}; do
|
||||
echo "${GITHUB_WORKSPACE}/codex-rs/target/${TARGET}/release/${binary}.exe"
|
||||
done
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0
|
||||
with:
|
||||
@@ -50,8 +70,4 @@ runs:
|
||||
exclude-azure-developer-cli-credential: true
|
||||
exclude-interactive-browser-credential: true
|
||||
cache-dependencies: false
|
||||
files: |
|
||||
${{ github.workspace }}/codex-rs/target/${{ inputs.target }}/release/codex.exe
|
||||
${{ github.workspace }}/codex-rs/target/${{ inputs.target }}/release/codex-responses-api-proxy.exe
|
||||
${{ github.workspace }}/codex-rs/target/${{ inputs.target }}/release/codex-windows-sandbox-setup.exe
|
||||
${{ github.workspace }}/codex-rs/target/${{ inputs.target }}/release/codex-command-runner.exe
|
||||
files: ${{ steps.prepare.outputs.files }}
|
||||
|
||||
@@ -28,6 +28,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex-app-server": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"regex": "^codex-app-server-aarch64-apple-darwin\\.zst$",
|
||||
"path": "codex-app-server"
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"regex": "^codex-app-server-x86_64-apple-darwin\\.zst$",
|
||||
"path": "codex-app-server"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"regex": "^codex-app-server-x86_64-unknown-linux-musl\\.zst$",
|
||||
"path": "codex-app-server"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"regex": "^codex-app-server-aarch64-unknown-linux-musl\\.zst$",
|
||||
"path": "codex-app-server"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"regex": "^codex-app-server-x86_64-pc-windows-msvc\\.exe\\.zst$",
|
||||
"path": "codex-app-server.exe"
|
||||
},
|
||||
"windows-aarch64": {
|
||||
"regex": "^codex-app-server-aarch64-pc-windows-msvc\\.exe\\.zst$",
|
||||
"path": "codex-app-server.exe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex-responses-api-proxy": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<#
|
||||
BuildBuddy cache keys include the action and test environment, so Bazel should
|
||||
not inherit the full hosted-runner PATH on Windows. That PATH includes volatile
|
||||
tool entries, such as Maven, that can change independently of this repo and
|
||||
cause avoidable cache misses.
|
||||
|
||||
This script derives a smaller, cache-stable PATH that keeps the Windows
|
||||
toolchain entries Bazel-backed CI tasks need: MSVC and Windows SDK paths, Git,
|
||||
PowerShell, Node, Python, DotSlash, and the standard Windows system
|
||||
directories.
|
||||
`setup-bazel-ci` runs this after exporting the MSVC environment, and the script
|
||||
publishes the result via `GITHUB_ENV` as `CODEX_BAZEL_WINDOWS_PATH` so later
|
||||
steps can pass that explicit PATH to Bazel.
|
||||
#>
|
||||
|
||||
$stablePathEntries = New-Object System.Collections.Generic.List[string]
|
||||
$seenEntries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$windowsAppsPath = if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
|
||||
$null
|
||||
} else {
|
||||
"$($env:LOCALAPPDATA)\Microsoft\WindowsApps"
|
||||
}
|
||||
$windowsDir = if ($env:WINDIR) {
|
||||
$env:WINDIR
|
||||
} elseif ($env:SystemRoot) {
|
||||
$env:SystemRoot
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
function Add-StablePathEntry {
|
||||
param([string]$PathEntry)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($PathEntry)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($seenEntries.Add($PathEntry)) {
|
||||
[void]$stablePathEntries.Add($PathEntry)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pathEntry in ($env:PATH -split ';')) {
|
||||
if ([string]::IsNullOrWhiteSpace($pathEntry)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
$pathEntry -like '*Microsoft Visual Studio*' -or
|
||||
$pathEntry -like '*Windows Kits*' -or
|
||||
$pathEntry -like '*Microsoft SDKs*' -or
|
||||
$pathEntry -like 'C:\Program Files\Git\*' -or
|
||||
$pathEntry -like 'C:\Program Files\PowerShell\*' -or
|
||||
$pathEntry -like 'C:\hostedtoolcache\windows\node\*' -or
|
||||
$pathEntry -like 'C:\hostedtoolcache\windows\Python\*' -or
|
||||
$pathEntry -eq 'D:\a\_temp\install-dotslash\bin' -or
|
||||
($windowsDir -and ($pathEntry -eq $windowsDir -or $pathEntry -like "${windowsDir}\*"))
|
||||
) {
|
||||
Add-StablePathEntry $pathEntry
|
||||
}
|
||||
}
|
||||
|
||||
$gitCommand = Get-Command git -ErrorAction SilentlyContinue
|
||||
if ($gitCommand) {
|
||||
Add-StablePathEntry (Split-Path $gitCommand.Source -Parent)
|
||||
}
|
||||
|
||||
$nodeCommand = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($nodeCommand) {
|
||||
Add-StablePathEntry (Split-Path $nodeCommand.Source -Parent)
|
||||
}
|
||||
|
||||
$python3Command = Get-Command python3 -ErrorAction SilentlyContinue
|
||||
if ($python3Command) {
|
||||
Add-StablePathEntry (Split-Path $python3Command.Source -Parent)
|
||||
}
|
||||
|
||||
$pythonCommand = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($pythonCommand) {
|
||||
Add-StablePathEntry (Split-Path $pythonCommand.Source -Parent)
|
||||
}
|
||||
|
||||
$pwshCommand = Get-Command pwsh -ErrorAction SilentlyContinue
|
||||
if ($pwshCommand) {
|
||||
Add-StablePathEntry (Split-Path $pwshCommand.Source -Parent)
|
||||
}
|
||||
|
||||
if ($windowsAppsPath) {
|
||||
Add-StablePathEntry $windowsAppsPath
|
||||
}
|
||||
|
||||
if ($stablePathEntries.Count -eq 0) {
|
||||
throw 'Failed to derive cache-stable Windows PATH.'
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
|
||||
throw 'GITHUB_ENV must be set.'
|
||||
}
|
||||
|
||||
$stablePath = $stablePathEntries -join ';'
|
||||
Write-Host 'Derived CODEX_BAZEL_WINDOWS_PATH entries:'
|
||||
foreach ($pathEntry in $stablePathEntries) {
|
||||
Write-Host " $pathEntry"
|
||||
}
|
||||
"CODEX_BAZEL_WINDOWS_PATH=$stablePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
@@ -2,16 +2,6 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
macOS)
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
;;
|
||||
esac
|
||||
|
||||
bazel_lint_args=("$@")
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
has_host_platform_override=0
|
||||
@@ -44,29 +34,6 @@ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
bazel_lint_args+=("--skip_incompatible_explicit_targets")
|
||||
fi
|
||||
|
||||
bazel_startup_args=()
|
||||
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
run_bazel() {
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
bazel "$@"
|
||||
}
|
||||
|
||||
run_bazel_with_startup_args() {
|
||||
if [[ ${#bazel_startup_args[@]} -gt 0 ]]; then
|
||||
run_bazel "${bazel_startup_args[@]}" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
run_bazel "$@"
|
||||
}
|
||||
|
||||
read_query_labels() {
|
||||
local query="$1"
|
||||
local query_stdout
|
||||
@@ -74,12 +41,10 @@ read_query_labels() {
|
||||
query_stdout="$(mktemp)"
|
||||
query_stderr="$(mktemp)"
|
||||
|
||||
if ! run_bazel_with_startup_args \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
query \
|
||||
if ! ./.github/scripts/run-bazel-query-ci.sh \
|
||||
--keep_going \
|
||||
--output=label \
|
||||
"$query" >"$query_stdout" 2>"$query_stderr"; then
|
||||
-- "$query" >"$query_stdout" 2>"$query_stderr"; then
|
||||
cat "$query_stderr" >&2
|
||||
rm -f "$query_stdout" "$query_stderr"
|
||||
exit 1
|
||||
|
||||
@@ -4,7 +4,6 @@ set -euo pipefail
|
||||
|
||||
print_failed_bazel_test_logs=0
|
||||
print_failed_bazel_action_summary=0
|
||||
use_node_test_env=0
|
||||
remote_download_toplevel=0
|
||||
windows_msvc_host_platform=0
|
||||
|
||||
@@ -18,10 +17,6 @@ while [[ $# -gt 0 ]]; do
|
||||
print_failed_bazel_action_summary=1
|
||||
shift
|
||||
;;
|
||||
--use-node-test-env)
|
||||
use_node_test_env=1
|
||||
shift
|
||||
;;
|
||||
--remote-download-toplevel)
|
||||
remote_download_toplevel=1
|
||||
shift
|
||||
@@ -42,7 +37,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--print-failed-action-summary] [--use-node-test-env] [--remote-download-toplevel] [--windows-msvc-host-platform] -- <bazel args> -- <targets>" >&2
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--print-failed-action-summary] [--remote-download-toplevel] [--windows-msvc-host-platform] -- <bazel args> -- <targets>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -249,16 +244,6 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $use_node_test_env -eq 1 ]]; then
|
||||
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
|
||||
# before the `actions/setup-node` runtime on PATH.
|
||||
node_bin="$(which node)"
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
node_bin="$(cygpath -w "${node_bin}")"
|
||||
fi
|
||||
bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}")
|
||||
fi
|
||||
|
||||
post_config_bazel_args=()
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_msvc_host_platform -eq 1 ]]; then
|
||||
has_host_platform_override=0
|
||||
@@ -306,7 +291,6 @@ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
INCLUDE
|
||||
LIB
|
||||
LIBPATH
|
||||
PATH
|
||||
UCRTVersion
|
||||
UniversalCRTSdkDir
|
||||
VCINSTALLDIR
|
||||
@@ -323,6 +307,17 @@ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
post_config_bazel_args+=("--action_env=${env_var}" "--host_action_env=${env_var}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "${CODEX_BAZEL_WINDOWS_PATH:-}" ]]; then
|
||||
echo "CODEX_BAZEL_WINDOWS_PATH must be set for Windows Bazel CI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
post_config_bazel_args+=(
|
||||
"--action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
"--host_action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
"--test_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
)
|
||||
fi
|
||||
|
||||
bazel_console_log="$(mktemp)"
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Run Bazel queries with the same CI startup settings as the main build/test
|
||||
# invocation so target-discovery queries can reuse the same Bazel server.
|
||||
|
||||
query_args=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
query_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
query_expression="$1"
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
macOS)
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
;;
|
||||
esac
|
||||
|
||||
bazel_startup_args=()
|
||||
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
run_bazel() {
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
bazel "$@"
|
||||
}
|
||||
|
||||
bazel_query_args=(--noexperimental_remote_repo_contents_cache query)
|
||||
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
bazel_query_args+=(
|
||||
"--config=${ci_config}"
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
|
||||
bazel_query_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
|
||||
fi
|
||||
|
||||
if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
|
||||
bazel_query_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
|
||||
fi
|
||||
|
||||
bazel_query_args+=("${query_args[@]}" "$query_expression")
|
||||
|
||||
if (( ${#bazel_startup_args[@]} > 0 )); then
|
||||
run_bazel "${bazel_startup_args[@]}" "${bazel_query_args[@]}"
|
||||
else
|
||||
run_bazel "${bazel_query_args[@]}"
|
||||
fi
|
||||
@@ -8,25 +8,9 @@ FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl git python3 ca-certificates xz-utils && \
|
||||
curl git python3 ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY codex-rs/node-version.txt /tmp/node-version.txt
|
||||
|
||||
RUN set -eux; \
|
||||
node_arch="$(dpkg --print-architecture)"; \
|
||||
case "${node_arch}" in \
|
||||
amd64) node_dist_arch="x64" ;; \
|
||||
arm64) node_dist_arch="arm64" ;; \
|
||||
*) echo "unsupported architecture: ${node_arch}"; exit 1 ;; \
|
||||
esac; \
|
||||
node_version="$(tr -d '[:space:]' </tmp/node-version.txt)"; \
|
||||
curl -fsSLO "https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-${node_dist_arch}.tar.xz"; \
|
||||
tar -xJf "node-v${node_version}-linux-${node_dist_arch}.tar.xz" -C /usr/local --strip-components=1; \
|
||||
rm "node-v${node_version}-linux-${node_dist_arch}.tar.xz" /tmp/node-version.txt; \
|
||||
node --version; \
|
||||
npm --version
|
||||
|
||||
# Install dotslash.
|
||||
RUN curl -LSfs "https://github.com/facebook/dotslash/releases/download/v0.5.8/dotslash-ubuntu-22.04.$(uname -m).tar.gz" | tar fxz - -C /usr/local/bin
|
||||
|
||||
|
||||
@@ -17,6 +17,13 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
jobs:
|
||||
test:
|
||||
# Even though a no-cache-hit Windows build seems to exceed the 30-minute
|
||||
# limit on occasion, the more common reason for exceeding the limit is a
|
||||
# true test failure in a rust_test() marked "flaky" that gets run 3x.
|
||||
# In that case, extra time generally does not give us more signal.
|
||||
#
|
||||
# Ultimately we need true distributed builds (e.g.,
|
||||
# https://www.buildbuddy.io/docs/rbe-setup/) to speed things up.
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -85,7 +92,6 @@ jobs:
|
||||
|
||||
bazel_wrapper_args=(
|
||||
--print-failed-test-logs
|
||||
--use-node-test-env
|
||||
)
|
||||
bazel_test_args=(
|
||||
test
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
# .github/prompts/issue-deduplicator.txt file is obsolete and removed.
|
||||
- id: codex-all
|
||||
name: Find duplicates (pass 1, all issues)
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
- id: codex-open
|
||||
name: Find duplicates (pass 2, open issues)
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- id: codex
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
|
||||
@@ -560,10 +560,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up Node.js for js_repl tests
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: codex-rs/node-version.txt
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
|
||||
@@ -40,28 +40,42 @@ jobs:
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: primary
|
||||
build_args: --bin codex --bin codex-responses-api-proxy
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: primary
|
||||
build_args: --bin codex --bin codex-responses-api-proxy
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: helpers
|
||||
build_args: --bin codex-windows-sandbox-setup --bin codex-command-runner
|
||||
binaries: "codex-windows-sandbox-setup codex-command-runner"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: helpers
|
||||
build_args: --bin codex-windows-sandbox-setup --bin codex-command-runner
|
||||
binaries: "codex-windows-sandbox-setup codex-command-runner"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: app-server
|
||||
binaries: "codex-app-server"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: app-server
|
||||
binaries: "codex-app-server"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
@@ -89,7 +103,11 @@ jobs:
|
||||
- name: Cargo build (Windows binaries)
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build --target ${{ matrix.target }} --release --timings ${{ matrix.build_args }}
|
||||
build_args=()
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
build_args+=(--bin "$binary")
|
||||
done
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
@@ -103,13 +121,9 @@ jobs:
|
||||
run: |
|
||||
output_dir="target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}"
|
||||
mkdir -p "$output_dir"
|
||||
if [[ "${{ matrix.bundle }}" == "primary" ]]; then
|
||||
cp target/${{ matrix.target }}/release/codex.exe "$output_dir/codex.exe"
|
||||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$output_dir/codex-responses-api-proxy.exe"
|
||||
else
|
||||
cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$output_dir/codex-windows-sandbox-setup.exe"
|
||||
cp target/${{ matrix.target }}/release/codex-command-runner.exe "$output_dir/codex-command-runner.exe"
|
||||
fi
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
cp "target/${{ matrix.target }}/release/${binary}.exe" "$output_dir/${binary}.exe"
|
||||
done
|
||||
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
@@ -130,6 +144,8 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
WINDOWS_BINARIES: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server"
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -161,19 +177,25 @@ jobs:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows app-server binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-app-server
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Verify binaries
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ls -lh target/${{ matrix.target }}/release/codex.exe
|
||||
ls -lh target/${{ matrix.target }}/release/codex-responses-api-proxy.exe
|
||||
ls -lh target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe
|
||||
ls -lh target/${{ matrix.target }}/release/codex-command-runner.exe
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
ls -lh "target/${{ matrix.target }}/release/${binary}.exe"
|
||||
done
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
uses: ./.github/actions/windows-code-sign
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
binaries: ${{ env.WINDOWS_BINARIES }}
|
||||
client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }}
|
||||
@@ -187,10 +209,10 @@ jobs:
|
||||
dest="dist/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
|
||||
cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe"
|
||||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe"
|
||||
cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe"
|
||||
cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe"
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
cp "target/${{ matrix.target }}/release/${binary}.exe" \
|
||||
"$dest/${binary}-${{ matrix.target }}.exe"
|
||||
done
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
build:
|
||||
needs: tag-check
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
@@ -67,16 +67,53 @@ jobs:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: aarch64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
build_dmg: "false"
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: x86_64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
build_dmg: "false"
|
||||
# Release artifacts intentionally ship MUSL-linked Linux binaries.
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
bundle: primary
|
||||
artifact_name: x86_64-unknown-linux-musl
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
build_dmg: "false"
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
target: x86_64-unknown-linux-musl
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-unknown-linux-musl-app-server
|
||||
binaries: "codex-app-server"
|
||||
build_dmg: "false"
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
bundle: primary
|
||||
artifact_name: aarch64-unknown-linux-musl
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
build_dmg: "false"
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
target: aarch64-unknown-linux-musl
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-unknown-linux-musl-app-server
|
||||
binaries: "codex-app-server"
|
||||
build_dmg: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -219,13 +256,17 @@ jobs:
|
||||
- name: Cargo build
|
||||
shell: bash
|
||||
run: |
|
||||
build_args=()
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
build_args+=(--bin "$binary")
|
||||
done
|
||||
echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}"
|
||||
cargo build --target ${{ matrix.target }} --release --timings --bin codex --bin codex-responses-api-proxy
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: cargo-timings-rust-release-${{ matrix.target }}
|
||||
name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -235,12 +276,14 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release
|
||||
binaries: ${{ matrix.binaries }}
|
||||
|
||||
- if: ${{ runner.os == 'macOS' }}
|
||||
name: MacOS code signing (binaries)
|
||||
uses: ./.github/actions/macos-code-sign
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
binaries: ${{ matrix.binaries }}
|
||||
sign-binaries: "true"
|
||||
sign-dmg: "false"
|
||||
apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
@@ -249,7 +292,7 @@ jobs:
|
||||
apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
|
||||
- if: ${{ runner.os == 'macOS' }}
|
||||
- if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' }}
|
||||
name: Build macOS dmg
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -264,23 +307,17 @@ jobs:
|
||||
# The previous "MacOS code signing (binaries)" step signs + notarizes the
|
||||
# built artifacts in `${release_dir}`. This step packages *those same*
|
||||
# signed binaries into a dmg.
|
||||
codex_binary_path="${release_dir}/codex"
|
||||
proxy_binary_path="${release_dir}/codex-responses-api-proxy"
|
||||
|
||||
rm -rf "$dmg_root"
|
||||
mkdir -p "$dmg_root"
|
||||
|
||||
if [[ ! -f "$codex_binary_path" ]]; then
|
||||
echo "Binary $codex_binary_path not found"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$proxy_binary_path" ]]; then
|
||||
echo "Binary $proxy_binary_path not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ditto "$codex_binary_path" "${dmg_root}/codex"
|
||||
ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy"
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
binary_path="${release_dir}/${binary}"
|
||||
if [[ ! -f "${binary_path}" ]]; then
|
||||
echo "Binary ${binary_path} not found"
|
||||
exit 1
|
||||
fi
|
||||
ditto "${binary_path}" "${dmg_root}/${binary}"
|
||||
done
|
||||
|
||||
rm -f "$dmg_path"
|
||||
hdiutil create \
|
||||
@@ -295,7 +332,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- if: ${{ runner.os == 'macOS' }}
|
||||
- if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' }}
|
||||
name: MacOS code signing (dmg)
|
||||
uses: ./.github/actions/macos-code-sign
|
||||
with:
|
||||
@@ -314,15 +351,15 @@ jobs:
|
||||
dest="dist/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
|
||||
cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
|
||||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}"
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
|
||||
if [[ "${{ matrix.target }}" == *linux* ]]; then
|
||||
cp "target/${{ matrix.target }}/release/${binary}.sigstore" \
|
||||
"$dest/${binary}-${{ matrix.target }}.sigstore"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${{ matrix.target }}" == *linux* ]]; then
|
||||
cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore"
|
||||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore"
|
||||
fi
|
||||
|
||||
if [[ "${{ matrix.target }}" == *apple-darwin ]]; then
|
||||
if [[ "${{ matrix.build_dmg }}" == "true" ]]; then
|
||||
cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg"
|
||||
fi
|
||||
|
||||
@@ -364,7 +401,7 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
name: ${{ matrix.artifact_name }}
|
||||
# Upload the per-binary .zst files as well as the new .tar.gz
|
||||
# equivalents we generated in the previous step.
|
||||
path: |
|
||||
@@ -614,11 +651,59 @@ jobs:
|
||||
prefix="${NPM_TAG}-"
|
||||
fi
|
||||
|
||||
root_tarball="dist/npm/codex-npm-${VERSION}.tgz"
|
||||
sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz"
|
||||
# Keep this list in sync with CODEX_PLATFORM_PACKAGES in
|
||||
# codex-cli/scripts/build_npm_package.py. The root wrapper advances
|
||||
# @openai/codex@latest as soon as it publishes, so every platform
|
||||
# package it aliases must already exist in the registry first.
|
||||
platform_tarballs=(
|
||||
"dist/npm/codex-npm-linux-x64-${VERSION}.tgz"
|
||||
"dist/npm/codex-npm-linux-arm64-${VERSION}.tgz"
|
||||
"dist/npm/codex-npm-darwin-x64-${VERSION}.tgz"
|
||||
"dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz"
|
||||
"dist/npm/codex-npm-win32-x64-${VERSION}.tgz"
|
||||
"dist/npm/codex-npm-win32-arm64-${VERSION}.tgz"
|
||||
)
|
||||
|
||||
for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do
|
||||
if [[ ! -f "${required_tarball}" ]]; then
|
||||
echo "Missing npm tarball: ${required_tarball}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
shopt -s nullglob
|
||||
tarballs=(dist/npm/*-"${VERSION}".tgz)
|
||||
if [[ ${#tarballs[@]} -eq 0 ]]; then
|
||||
echo "No npm tarballs found in dist/npm for version ${VERSION}"
|
||||
exit 1
|
||||
other_tarballs=()
|
||||
for tarball in dist/npm/*-"${VERSION}".tgz; do
|
||||
if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
is_platform_tarball=false
|
||||
for platform_tarball in "${platform_tarballs[@]}"; do
|
||||
if [[ "${tarball}" == "${platform_tarball}" ]]; then
|
||||
is_platform_tarball=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "${is_platform_tarball}" == true ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
other_tarballs+=("${tarball}")
|
||||
done
|
||||
|
||||
# Publish the platform packages before the root CLI wrapper. The root
|
||||
# wrapper advances @openai/codex@latest, so it should only publish
|
||||
# after the optional dependency versions it references exist.
|
||||
tarballs=(
|
||||
"${platform_tarballs[@]}"
|
||||
"${other_tarballs[@]}"
|
||||
"${root_tarball}"
|
||||
)
|
||||
if [[ -f "${sdk_tarball}" ]]; then
|
||||
tarballs+=("${sdk_tarball}")
|
||||
fi
|
||||
|
||||
for tarball in "${tarballs[@]}"; do
|
||||
|
||||
+1
-1
@@ -52,6 +52,7 @@ yarn-error.log*
|
||||
# env
|
||||
.env*
|
||||
!.env.example
|
||||
.venv/
|
||||
|
||||
# package
|
||||
*.tgz
|
||||
@@ -91,4 +92,3 @@ CHANGELOG.ignore.md
|
||||
# Python bytecode files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
|
||||
@@ -4,6 +4,3 @@ Copyright 2025 OpenAI
|
||||
This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023-2025 The Ratatui Developers
|
||||
|
||||
This project includes Meriyah parser assets from [meriyah](https://github.com/meriyah/meriyah), licensed under the ISC license.
|
||||
Copyright (c) 2019 and later, KFlash and others.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
node_modules/
|
||||
@@ -1,59 +0,0 @@
|
||||
FROM node:24-slim
|
||||
|
||||
ARG TZ
|
||||
ENV TZ="$TZ"
|
||||
|
||||
# Install basic development tools, ca-certificates, and iptables/ipset, then clean up apt cache to reduce image size
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
aggregate \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dnsutils \
|
||||
fzf \
|
||||
gh \
|
||||
git \
|
||||
gnupg2 \
|
||||
iproute2 \
|
||||
ipset \
|
||||
iptables \
|
||||
jq \
|
||||
less \
|
||||
man-db \
|
||||
procps \
|
||||
unzip \
|
||||
ripgrep \
|
||||
zsh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Ensure default node user has access to /usr/local/share
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
chown -R node:node /usr/local/share
|
||||
|
||||
ARG USERNAME=node
|
||||
|
||||
# Set up non-root user
|
||||
USER node
|
||||
|
||||
# Install global packages
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Install codex
|
||||
COPY dist/codex.tgz codex.tgz
|
||||
RUN npm install -g codex.tgz \
|
||||
&& npm cache clean --force \
|
||||
&& rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/node_modules/.cache \
|
||||
&& rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/tests \
|
||||
&& rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/docs
|
||||
|
||||
# Inside the container we consider the environment already sufficiently locked
|
||||
# down, therefore instruct Codex CLI to allow running without sandboxing.
|
||||
ENV CODEX_UNSAFE_ALLOW_NO_SANDBOX=1
|
||||
|
||||
# Copy and set up firewall script as root.
|
||||
USER root
|
||||
COPY scripts/init_firewall.sh /usr/local/bin/
|
||||
RUN chmod 500 /usr/local/bin/init_firewall.sh
|
||||
|
||||
# Drop back to non-root.
|
||||
USER node
|
||||
@@ -18,5 +18,5 @@
|
||||
"url": "git+https://github.com/openai/codex.git",
|
||||
"directory": "codex-cli"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(realpath "$(dirname "$0")")
|
||||
trap "popd >> /dev/null" EXIT
|
||||
pushd "$SCRIPT_DIR/.." >> /dev/null || {
|
||||
echo "Error: Failed to change directory to $SCRIPT_DIR/.."
|
||||
exit 1
|
||||
}
|
||||
pnpm install
|
||||
pnpm run build
|
||||
rm -rf ./dist/openai-codex-*.tgz
|
||||
pnpm pack --pack-destination ./dist
|
||||
mv ./dist/openai-codex-*.tgz ./dist/codex.tgz
|
||||
docker build -t codex -f "./Dockerfile" .
|
||||
@@ -1,6 +1,5 @@
|
||||
exports_files([
|
||||
"clippy.toml",
|
||||
"node-version.txt",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
|
||||
Generated
+71
-21
@@ -1758,6 +1758,7 @@ dependencies = [
|
||||
"codex-protocol",
|
||||
"crypto_box",
|
||||
"ed25519-dalek",
|
||||
"jsonwebtoken",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"reqwest",
|
||||
@@ -1773,6 +1774,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -1840,6 +1842,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
@@ -1856,6 +1859,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
@@ -2045,9 +2049,11 @@ name = "codex-backend-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-api",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
@@ -2071,11 +2077,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"pretty_assertions",
|
||||
@@ -2094,7 +2100,6 @@ dependencies = [
|
||||
"assert_matches",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"codex-api",
|
||||
"codex-app-server",
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-test-client",
|
||||
@@ -2111,11 +2116,11 @@ dependencies = [
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-mcp-server",
|
||||
"codex-model-provider",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
"codex-rmcp-client",
|
||||
"codex-rollout-trace",
|
||||
"codex-sandboxing",
|
||||
"codex-state",
|
||||
"codex-stdio-to-uds",
|
||||
@@ -2203,7 +2208,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-client",
|
||||
@@ -2212,6 +2216,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-tui",
|
||||
"codex-utils-cli",
|
||||
"crossterm",
|
||||
@@ -2236,6 +2241,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-backend-client",
|
||||
"codex-git-utils",
|
||||
"serde",
|
||||
@@ -2280,15 +2286,20 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"codex-app-server-protocol",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-git-utils",
|
||||
"codex-model-provider-info",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path",
|
||||
"core-foundation 0.9.4",
|
||||
"dns-lookup",
|
||||
"dunce",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"libc",
|
||||
@@ -2312,6 +2323,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"wildmatch",
|
||||
"winapi-util",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2358,6 +2370,8 @@ dependencies = [
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-memories-read",
|
||||
"codex-memories-write",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -2392,7 +2406,6 @@ dependencies = [
|
||||
"codex-utils-string",
|
||||
"codex-utils-template",
|
||||
"codex-windows-sandbox",
|
||||
"core-foundation 0.9.4",
|
||||
"core_test_support",
|
||||
"csv",
|
||||
"ctor 0.6.3",
|
||||
@@ -2443,7 +2456,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
"which 8.0.0",
|
||||
"whoami",
|
||||
"windows-sys 0.52.0",
|
||||
"wiremock",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
@@ -2460,6 +2472,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
@@ -2491,6 +2504,7 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-skills",
|
||||
@@ -2527,6 +2541,7 @@ dependencies = [
|
||||
name = "codex-device-key"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"p256",
|
||||
"pretty_assertions",
|
||||
@@ -2534,6 +2549,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -2549,6 +2565,7 @@ dependencies = [
|
||||
"codex-apply-patch",
|
||||
"codex-arg0",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
@@ -2592,7 +2609,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-test-binary-support",
|
||||
@@ -2770,7 +2786,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"clap",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
@@ -2849,15 +2864,16 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"codex-api",
|
||||
"codex-async-utils",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-plugins",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
@@ -2907,19 +2923,62 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-memories-read"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-output-truncation",
|
||||
"codex-utils-template",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-memories-write"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codex-git-utils",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-state",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-output-truncation",
|
||||
"codex-utils-template",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-model-provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-agent-identity",
|
||||
"codex-api",
|
||||
"codex-aws-auth",
|
||||
"codex-client",
|
||||
"codex-feedback",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-response-debug-context",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2943,32 +3002,21 @@ dependencies = [
|
||||
name = "codex-models-manager"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-collaboration-mode-templates",
|
||||
"codex-config",
|
||||
"codex-feedback",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-response-debug-context",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-output-truncation",
|
||||
"codex-utils-template",
|
||||
"core_test_support",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3108,6 +3156,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3154,6 +3203,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"codex-api",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
|
||||
@@ -46,6 +46,8 @@ members = [
|
||||
"login",
|
||||
"codex-mcp",
|
||||
"mcp-server",
|
||||
"memories/read",
|
||||
"memories/write",
|
||||
"model-provider-info",
|
||||
"models-manager",
|
||||
"network-proxy",
|
||||
@@ -153,6 +155,8 @@ codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-memories-read = { path = "memories/read" }
|
||||
codex-memories-write = { path = "memories/write" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-model-provider-info = { path = "model-provider-info" }
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ In `workspace-write`, Codex also includes `~/.codex/memories` in its writable ro
|
||||
|
||||
This folder is the root of a Cargo workspace. It contains quite a bit of experimental code, but here are the key crates:
|
||||
|
||||
- [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this to be a library crate that is generally useful for building other Rust/native applications that use Codex.
|
||||
- [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this becomes a library crate that is generally useful for building other Rust/native applications that use Codex.
|
||||
- [`exec/`](./exec) "headless" CLI for use in automation.
|
||||
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
|
||||
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.
|
||||
|
||||
@@ -19,6 +19,7 @@ chrono = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
crypto_box = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -8,6 +8,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use crypto_box::SecretKey as Curve25519SecretKey;
|
||||
use ed25519_dalek::Signer as _;
|
||||
@@ -15,10 +16,14 @@ use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use ed25519_dalek::pkcs8::DecodePrivateKey;
|
||||
use ed25519_dalek::pkcs8::EncodePrivateKey;
|
||||
use jsonwebtoken::Algorithm;
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use jsonwebtoken::Validation;
|
||||
use rand::TryRngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use sha2::Digest as _;
|
||||
use sha2::Sha512;
|
||||
|
||||
@@ -50,6 +55,18 @@ pub struct GeneratedAgentKeyMaterial {
|
||||
pub public_key_ssh: String,
|
||||
}
|
||||
|
||||
/// Claims carried by an Agent Identity JWT.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct AgentIdentityJwtClaims {
|
||||
pub agent_runtime_id: String,
|
||||
pub agent_private_key: String,
|
||||
pub account_id: String,
|
||||
pub chatgpt_user_id: String,
|
||||
pub email: String,
|
||||
pub plan_type: AccountPlanType,
|
||||
pub chatgpt_account_is_fedramp: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct AgentAssertionEnvelope {
|
||||
agent_runtime_id: String,
|
||||
@@ -98,6 +115,43 @@ pub fn authorization_header_for_agent_task(
|
||||
Ok(format!("AgentAssertion {serialized_assertion}"))
|
||||
}
|
||||
|
||||
pub fn decode_agent_identity_jwt(
|
||||
jwt: &str,
|
||||
public_key_base64: Option<&str>,
|
||||
) -> Result<AgentIdentityJwtClaims> {
|
||||
let Some(public_key_base64) = public_key_base64 else {
|
||||
return decode_agent_identity_jwt_payload(jwt);
|
||||
};
|
||||
|
||||
let mut validation = Validation::new(Algorithm::EdDSA);
|
||||
validation.required_spec_claims.clear();
|
||||
validation.validate_exp = false;
|
||||
validation.validate_aud = false;
|
||||
|
||||
let public_key = BASE64_STANDARD
|
||||
.decode(public_key_base64)
|
||||
.context("agent identity JWT public key is not valid base64")?;
|
||||
let decoding_key = DecodingKey::from_ed_der(&public_key);
|
||||
|
||||
jsonwebtoken::decode::<AgentIdentityJwtClaims>(jwt, &decoding_key, &validation)
|
||||
.map(|data| data.claims)
|
||||
.context("failed to decode agent identity JWT")
|
||||
}
|
||||
|
||||
fn decode_agent_identity_jwt_payload<T: DeserializeOwned>(jwt: &str) -> Result<T> {
|
||||
let mut parts = jwt.split('.');
|
||||
let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
|
||||
_ => anyhow::bail!("invalid agent identity JWT format"),
|
||||
};
|
||||
anyhow::ensure!(parts.next().is_none(), "invalid agent identity JWT format");
|
||||
|
||||
let payload_bytes = URL_SAFE_NO_PAD
|
||||
.decode(payload_b64)
|
||||
.context("agent identity JWT payload is not valid base64url")?;
|
||||
serde_json::from_slice(&payload_bytes).context("agent identity JWT payload is not valid JSON")
|
||||
}
|
||||
|
||||
pub fn sign_task_registration_payload(
|
||||
key: AgentIdentityKey<'_>,
|
||||
timestamp: &str,
|
||||
@@ -117,19 +171,27 @@ pub async fn register_agent_task(
|
||||
signature: sign_task_registration_payload(key, ×tamp)?,
|
||||
timestamp,
|
||||
};
|
||||
let url = agent_task_registration_url(chatgpt_base_url, key.agent_runtime_id);
|
||||
|
||||
let response = client
|
||||
.post(agent_task_registration_url(
|
||||
chatgpt_base_url,
|
||||
key.agent_runtime_id,
|
||||
))
|
||||
.post(url)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to register agent task")?
|
||||
.error_for_status()
|
||||
.context("failed to register agent task")?
|
||||
.context("failed to register agent task")?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let body = if body.len() > 512 {
|
||||
format!("{}...", body.chars().take(512).collect::<String>())
|
||||
} else {
|
||||
body
|
||||
};
|
||||
anyhow::bail!("failed to register agent task with status {status}: {body}");
|
||||
}
|
||||
|
||||
let response = response
|
||||
.json()
|
||||
.await
|
||||
.context("failed to decode agent task registration response")?;
|
||||
@@ -323,6 +385,8 @@ mod tests {
|
||||
use base64::Engine as _;
|
||||
use ed25519_dalek::Signature;
|
||||
use ed25519_dalek::Verifier as _;
|
||||
use jsonwebtoken::EncodingKey;
|
||||
use jsonwebtoken::Header;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
@@ -404,6 +468,119 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_agent_identity_jwt_reads_claims() {
|
||||
let jwt = jwt_with_payload(serde_json::json!({
|
||||
"agent_runtime_id": "agent-runtime-id",
|
||||
"agent_private_key": "private-key",
|
||||
"account_id": "account-id",
|
||||
"chatgpt_user_id": "user-id",
|
||||
"email": "user@example.com",
|
||||
"plan_type": "pro",
|
||||
"chatgpt_account_is_fedramp": false,
|
||||
}));
|
||||
|
||||
let claims =
|
||||
decode_agent_identity_jwt(&jwt, /*public_key_base64*/ None).expect("JWT should decode");
|
||||
|
||||
assert_eq!(
|
||||
claims,
|
||||
AgentIdentityJwtClaims {
|
||||
agent_runtime_id: "agent-runtime-id".to_string(),
|
||||
agent_private_key: "private-key".to_string(),
|
||||
account_id: "account-id".to_string(),
|
||||
chatgpt_user_id: "user-id".to_string(),
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_agent_identity_jwt_verifies_when_public_key_is_present() {
|
||||
let mut secret_key_bytes = [0u8; 32];
|
||||
secret_key_bytes[0] = 1;
|
||||
let signing_key = SigningKey::from_bytes(&secret_key_bytes);
|
||||
let private_key_pkcs8 = signing_key
|
||||
.to_pkcs8_der()
|
||||
.expect("private key should encode");
|
||||
let public_key_base64 = BASE64_STANDARD.encode(signing_key.verifying_key().as_bytes());
|
||||
let claims = AgentIdentityJwtClaims {
|
||||
agent_runtime_id: "agent-runtime-id".to_string(),
|
||||
agent_private_key: "private-key".to_string(),
|
||||
account_id: "account-id".to_string(),
|
||||
chatgpt_user_id: "user-id".to_string(),
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
};
|
||||
let jwt = jsonwebtoken::encode(
|
||||
&Header::new(Algorithm::EdDSA),
|
||||
&serde_json::json!({
|
||||
"agent_runtime_id": claims.agent_runtime_id,
|
||||
"agent_private_key": claims.agent_private_key,
|
||||
"account_id": claims.account_id,
|
||||
"chatgpt_user_id": claims.chatgpt_user_id,
|
||||
"email": claims.email,
|
||||
"plan_type": "pro",
|
||||
"chatgpt_account_is_fedramp": claims.chatgpt_account_is_fedramp,
|
||||
}),
|
||||
&EncodingKey::from_ed_der(private_key_pkcs8.as_bytes()),
|
||||
)
|
||||
.expect("JWT should encode");
|
||||
|
||||
let expected_claims = AgentIdentityJwtClaims {
|
||||
agent_runtime_id: "agent-runtime-id".to_string(),
|
||||
agent_private_key: "private-key".to_string(),
|
||||
account_id: "account-id".to_string(),
|
||||
chatgpt_user_id: "user-id".to_string(),
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
};
|
||||
assert_eq!(
|
||||
decode_agent_identity_jwt(&jwt, Some(&public_key_base64)).expect("JWT should verify"),
|
||||
expected_claims
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_agent_identity_jwt_rejects_wrong_public_key() {
|
||||
let mut signing_secret_key_bytes = [0u8; 32];
|
||||
signing_secret_key_bytes[0] = 1;
|
||||
let signing_key = SigningKey::from_bytes(&signing_secret_key_bytes);
|
||||
let private_key_pkcs8 = signing_key
|
||||
.to_pkcs8_der()
|
||||
.expect("private key should encode");
|
||||
|
||||
let mut other_secret_key_bytes = [0u8; 32];
|
||||
other_secret_key_bytes[0] = 2;
|
||||
let other_public_key_base64 = BASE64_STANDARD.encode(
|
||||
SigningKey::from_bytes(&other_secret_key_bytes)
|
||||
.verifying_key()
|
||||
.as_bytes(),
|
||||
);
|
||||
|
||||
let jwt = jsonwebtoken::encode(
|
||||
&Header::new(Algorithm::EdDSA),
|
||||
&serde_json::json!({
|
||||
"agent_runtime_id": "agent-runtime-id",
|
||||
"agent_private_key": "private-key",
|
||||
"account_id": "account-id",
|
||||
"chatgpt_user_id": "user-id",
|
||||
"email": "user@example.com",
|
||||
"plan_type": "pro",
|
||||
"chatgpt_account_is_fedramp": false,
|
||||
}),
|
||||
&EncodingKey::from_ed_der(private_key_pkcs8.as_bytes()),
|
||||
)
|
||||
.expect("JWT should encode");
|
||||
|
||||
decode_agent_identity_jwt(&jwt, Some(&other_public_key_base64))
|
||||
.expect_err("JWT should not verify");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_chatgpt_base_url_strips_codex_before_backend_api() {
|
||||
assert_eq!(
|
||||
@@ -411,4 +588,12 @@ mod tests {
|
||||
"https://chatgpt.com/backend-api"
|
||||
);
|
||||
}
|
||||
|
||||
fn jwt_with_payload(payload: serde_json::Value) -> String {
|
||||
let encode = |bytes: &[u8]| URL_SAFE_NO_PAD.encode(bytes);
|
||||
let header_b64 = encode(br#"{"alg":"none","typ":"JWT"}"#);
|
||||
let payload_b64 = encode(&serde_json::to_vec(&payload).expect("payload should serialize"));
|
||||
let signature_b64 = encode(b"sig");
|
||||
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ workspace = true
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
|
||||
@@ -164,11 +164,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) -
|
||||
}
|
||||
|
||||
fn sample_permission_profile() -> AppServerPermissionProfile {
|
||||
CorePermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&test_path_buf("/tmp"),
|
||||
)
|
||||
.into()
|
||||
CorePermissionProfile::Disabled.into()
|
||||
}
|
||||
|
||||
fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata {
|
||||
@@ -322,7 +318,10 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
session_source: SessionSource::Exec,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: CorePermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
permission_profile_cwd: PathBuf::from("/tmp"),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
|
||||
@@ -312,16 +312,9 @@ async fn send_track_events(
|
||||
let Some(auth) = auth_manager.auth().await else {
|
||||
return;
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return;
|
||||
}
|
||||
let access_token = match auth.get_token() {
|
||||
Ok(token) => token,
|
||||
Err(_) => return,
|
||||
};
|
||||
let Some(account_id) = auth.get_account_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/codex/analytics-events/events");
|
||||
@@ -330,8 +323,7 @@ async fn send_track_events(
|
||||
let response = create_client()
|
||||
.post(&url)
|
||||
.timeout(ANALYTICS_EVENTS_TIMEOUT)
|
||||
.bearer_auth(&access_token)
|
||||
.header("chatgpt-account-id", &account_id)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
|
||||
@@ -26,7 +26,7 @@ use codex_app_server_protocol::TrackUsageLimitBannerParams;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::models::SandboxPermissions;
|
||||
use codex_protocol::protocol::GuardianAssessmentOutcome;
|
||||
use codex_protocol::protocol::GuardianCommandSource;
|
||||
@@ -199,17 +199,17 @@ pub enum GuardianApprovalRequestSource {
|
||||
pub enum GuardianReviewedAction {
|
||||
Shell {
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
},
|
||||
UnifiedExec {
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
tty: bool,
|
||||
},
|
||||
Execve {
|
||||
source: GuardianCommandSource,
|
||||
program: String,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
},
|
||||
ApplyPatch {},
|
||||
NetworkAccess {
|
||||
|
||||
@@ -13,12 +13,12 @@ use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -62,7 +62,8 @@ pub struct TurnResolvedConfigFact {
|
||||
pub session_source: SessionSource,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub permission_profile_cwd: PathBuf,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub reasoning_summary: Option<ReasoningSummary>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
|
||||
@@ -62,7 +62,7 @@ use codex_login::default_client::originator;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
@@ -891,7 +891,8 @@ fn codex_turn_event_params(
|
||||
session_source: _session_source,
|
||||
model,
|
||||
model_provider,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
permission_profile_cwd,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
service_tier,
|
||||
@@ -916,7 +917,10 @@ fn codex_turn_event_params(
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
model: Some(model),
|
||||
model_provider,
|
||||
sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)),
|
||||
sandbox_policy: Some(sandbox_policy_mode(
|
||||
&permission_profile,
|
||||
permission_profile_cwd.as_path(),
|
||||
)),
|
||||
reasoning_effort: reasoning_effort.map(|value| value.to_string()),
|
||||
reasoning_summary: reasoning_summary_mode(reasoning_summary),
|
||||
service_tier: service_tier
|
||||
@@ -961,12 +965,27 @@ fn codex_turn_event_params(
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str {
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => "full_access",
|
||||
SandboxPolicy::ReadOnly { .. } => "read_only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace_write",
|
||||
SandboxPolicy::ExternalSandbox { .. } => "external_sandbox",
|
||||
fn sandbox_policy_mode(permission_profile: &PermissionProfile, cwd: &Path) -> &'static str {
|
||||
match permission_profile {
|
||||
PermissionProfile::Disabled => "full_access",
|
||||
PermissionProfile::External { .. } => "external_sandbox",
|
||||
PermissionProfile::Managed { .. } => {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if file_system_policy.has_full_disk_write_access() {
|
||||
if permission_profile.network_sandbox_policy().is_enabled() {
|
||||
"full_access"
|
||||
} else {
|
||||
"external_sandbox"
|
||||
}
|
||||
} else if file_system_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.is_empty()
|
||||
{
|
||||
"read_only"
|
||||
} else {
|
||||
"workspace_write"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,3 +1076,25 @@ pub(crate) fn normalize_path_for_skill_id(
|
||||
_ => resolved_path.to_string_lossy().replace('\\', "/"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
|
||||
#[test]
|
||||
fn managed_full_disk_with_restricted_network_reports_external_sandbox() {
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::Managed,
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy_mode(&permission_profile, Path::new("/")),
|
||||
"external_sandbox"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
pub use codex_exec_server::EnvironmentManager;
|
||||
pub use codex_exec_server::EnvironmentManagerArgs;
|
||||
pub use codex_exec_server::ExecServerRuntimePaths;
|
||||
|
||||
+119
-112
@@ -1892,61 +1892,132 @@
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -2055,53 +2126,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RealtimeOutputModality": {
|
||||
"enum": [
|
||||
"text",
|
||||
@@ -2268,12 +2292,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -2985,16 +3003,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -3051,16 +3059,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -3387,6 +3385,15 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadInjectItemsParams": {
|
||||
"properties": {
|
||||
"items": {
|
||||
@@ -6163,4 +6170,4 @@
|
||||
}
|
||||
],
|
||||
"title": "ClientRequest"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -78,7 +78,8 @@
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "Partial overlay used for per-command permission requests."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -3028,6 +3028,93 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearedNotification": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadGoalUpdatedNotification": {
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4727,6 +4814,46 @@
|
||||
"title": "Thread/name/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/updated"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/cleared"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalClearedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
+2
-1
@@ -78,7 +78,8 @@
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "Partial overlay used for per-command permission requests."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+262
-116
@@ -25,7 +25,8 @@
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "Partial overlay used for per-command permission requests."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -3829,6 +3830,46 @@
|
||||
"title": "Thread/name/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/updated"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/cleared"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalClearedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -5244,6 +5285,22 @@
|
||||
],
|
||||
"title": "ChatgptAccount",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"amazonBedrock"
|
||||
],
|
||||
"title": "AmazonBedrockAccountType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "AmazonBedrockAccount",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11293,61 +11350,132 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -12164,53 +12292,6 @@
|
||||
"title": "RawResponseItemCompletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RealtimeConversationVersion": {
|
||||
"enum": [
|
||||
"v1",
|
||||
@@ -12682,12 +12763,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -13419,16 +13494,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -13485,16 +13550,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -14568,7 +14623,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -14614,6 +14669,97 @@
|
||||
"title": "ThreadForkResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadGoalUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/v2/ThreadGoal"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -16011,7 +16157,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -16338,7 +16484,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -17659,4 +17805,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+260
-115
@@ -46,6 +46,22 @@
|
||||
],
|
||||
"title": "ChatgptAccount",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"amazonBedrock"
|
||||
],
|
||||
"title": "AmazonBedrockAccountType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "AmazonBedrockAccount",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -8008,61 +8024,132 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -8879,53 +8966,6 @@
|
||||
"title": "RawResponseItemCompletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RealtimeConversationVersion": {
|
||||
"enum": [
|
||||
"v1",
|
||||
@@ -9397,12 +9437,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -10134,16 +10168,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -10200,16 +10224,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -10448,6 +10462,46 @@
|
||||
"title": "Thread/name/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/updated"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/cleared"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalClearedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -12455,7 +12509,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -12501,6 +12555,97 @@
|
||||
"title": "ThreadForkResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadGoalUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -13898,7 +14043,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -14225,7 +14370,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
@@ -15545,4 +15690,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocolV2",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+86
-82
@@ -246,110 +246,134 @@
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -370,16 +394,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -436,16 +450,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
|
||||
@@ -42,6 +42,22 @@
|
||||
],
|
||||
"title": "ChatgptAccount",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"amazonBedrock"
|
||||
],
|
||||
"title": "AmazonBedrockAccountType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "AmazonBedrockAccount",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-7
@@ -345,12 +345,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -928,4 +922,4 @@
|
||||
],
|
||||
"title": "RawResponseItemCompletedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+109
-38
@@ -276,61 +276,132 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SandboxMode": {
|
||||
|
||||
+87
-83
@@ -900,110 +900,134 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
@@ -1036,16 +1060,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -1102,16 +1116,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -2506,7 +2510,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalUpdatedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
+110
-45
@@ -542,61 +542,132 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -685,12 +756,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -1387,4 +1452,4 @@
|
||||
],
|
||||
"title": "ThreadResumeParams",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,110 +900,134 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
@@ -1036,16 +1060,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -1102,16 +1116,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -2506,7 +2510,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
|
||||
+124
-38
@@ -302,61 +302,132 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -388,6 +459,21 @@
|
||||
"clear"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"environmentId"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -900,110 +900,134 @@
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
@@ -1036,16 +1060,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -1102,16 +1116,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
@@ -2506,7 +2510,7 @@
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`."
|
||||
"description": "Canonical active permissions view for this thread."
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"anyOf": [
|
||||
|
||||
+109
-105
@@ -326,61 +326,132 @@
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"entries",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
@@ -391,53 +462,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"includePlatformDefaults": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedReadOnlyAccess",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"fullAccess"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccessType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "FullAccessReadOnlyAccess",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
@@ -490,16 +514,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"access": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
@@ -556,16 +570,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"readOnlyAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReadOnlyAccess"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"type": "fullAccess"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ReasoningItemContent } from "./ReasoningItemContent";
|
||||
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||
/**
|
||||
* Set when using the Responses API.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PlanType } from "../PlanType";
|
||||
|
||||
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string, planType: PlanType, };
|
||||
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string, planType: PlanType, } | { "type": "amazonBedrock", };
|
||||
|
||||
+5
-1
@@ -4,4 +4,8 @@
|
||||
import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
|
||||
export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, };
|
||||
export type AdditionalPermissionProfile = {
|
||||
/**
|
||||
* Partial overlay used for per-command permission requests.
|
||||
*/
|
||||
network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, };
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
import type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions";
|
||||
import type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions";
|
||||
|
||||
export type PermissionProfile = { network: PermissionProfileNetworkPermissions | null, fileSystem: PermissionProfileFileSystemPermissions | null, };
|
||||
export type PermissionProfile = { "type": "managed", network: PermissionProfileNetworkPermissions, fileSystem: PermissionProfileFileSystemPermissions, } | { "type": "disabled" } | { "type": "external", network: PermissionProfileNetworkPermissions, };
|
||||
|
||||
Generated
+1
-1
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
|
||||
|
||||
export type PermissionProfileFileSystemPermissions = { entries: Array<FileSystemSandboxEntry>, globScanMaxDepth?: number, };
|
||||
export type PermissionProfileFileSystemPermissions = { "type": "restricted", entries: Array<FileSystemSandboxEntry>, globScanMaxDepth?: number, } | { "type": "unrestricted" };
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PermissionProfileNetworkPermissions = { enabled: boolean | null, };
|
||||
export type PermissionProfileNetworkPermissions = { enabled: boolean, };
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type ReadOnlyAccess = { "type": "restricted", includePlatformDefaults: boolean, readableRoots: Array<AbsolutePathBuf>, } | { "type": "fullAccess" };
|
||||
@@ -3,6 +3,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { NetworkAccess } from "./NetworkAccess";
|
||||
import type { ReadOnlyAccess } from "./ReadOnlyAccess";
|
||||
|
||||
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", access: ReadOnlyAccess, networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array<AbsolutePathBuf>, readOnlyAccess: ReadOnlyAccess, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
|
||||
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array<AbsolutePathBuf>, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
|
||||
|
||||
@@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer,
|
||||
*/
|
||||
sandbox: SandboxPolicy,
|
||||
/**
|
||||
* Canonical active permissions view for this thread when representable.
|
||||
* This is `null` for external sandbox policies because external
|
||||
* enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
* Canonical active permissions view for this thread.
|
||||
*/
|
||||
permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadGoalStatus } from "./ThreadGoalStatus";
|
||||
|
||||
export type ThreadGoal = { threadId: string, objective: string, status: ThreadGoalStatus, tokenBudget: number | null, tokensUsed: number, timeUsedSeconds: number, createdAt: number, updatedAt: number, };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadGoalClearedNotification = { threadId: string, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadGoalStatus = "active" | "paused" | "budgetLimited" | "complete";
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadGoal } from "./ThreadGoal";
|
||||
|
||||
export type ThreadGoalUpdatedNotification = { threadId: string, turnId: string | null, goal: ThreadGoal, };
|
||||
@@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer,
|
||||
*/
|
||||
sandbox: SandboxPolicy,
|
||||
/**
|
||||
* Canonical active permissions view for this thread when representable.
|
||||
* This is `null` for external sandbox policies because external
|
||||
* enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
* Canonical active permissions view for this thread.
|
||||
*/
|
||||
permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
@@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer,
|
||||
*/
|
||||
sandbox: SandboxPolicy,
|
||||
/**
|
||||
* Canonical active permissions view for this thread when representable.
|
||||
* This is `null` for external sandbox policies because external
|
||||
* enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
* Canonical active permissions view for this thread.
|
||||
*/
|
||||
permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
+4
-1
@@ -276,7 +276,6 @@ export type { RateLimitReachedType } from "./RateLimitReachedType";
|
||||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||||
export type { RateLimitWindow } from "./RateLimitWindow";
|
||||
export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification";
|
||||
export type { ReadOnlyAccess } from "./ReadOnlyAccess";
|
||||
export type { ReasoningEffortOption } from "./ReasoningEffortOption";
|
||||
export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification";
|
||||
export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification";
|
||||
@@ -327,6 +326,10 @@ export type { ThreadCompactStartParams } from "./ThreadCompactStartParams";
|
||||
export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
|
||||
export type { ThreadForkParams } from "./ThreadForkParams";
|
||||
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
||||
export type { ThreadGoal } from "./ThreadGoal";
|
||||
export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification";
|
||||
export type { ThreadGoalStatus } from "./ThreadGoalStatus";
|
||||
export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification";
|
||||
export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams";
|
||||
export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse";
|
||||
export type { ThreadItem } from "./ThreadItem";
|
||||
|
||||
@@ -285,6 +285,21 @@ client_request_definitions! {
|
||||
params: v2::ThreadSetNameParams,
|
||||
response: v2::ThreadSetNameResponse,
|
||||
},
|
||||
#[experimental("thread/goal/set")]
|
||||
ThreadGoalSet => "thread/goal/set" {
|
||||
params: v2::ThreadGoalSetParams,
|
||||
response: v2::ThreadGoalSetResponse,
|
||||
},
|
||||
#[experimental("thread/goal/get")]
|
||||
ThreadGoalGet => "thread/goal/get" {
|
||||
params: v2::ThreadGoalGetParams,
|
||||
response: v2::ThreadGoalGetResponse,
|
||||
},
|
||||
#[experimental("thread/goal/clear")]
|
||||
ThreadGoalClear => "thread/goal/clear" {
|
||||
params: v2::ThreadGoalClearParams,
|
||||
response: v2::ThreadGoalClearResponse,
|
||||
},
|
||||
ThreadMetadataUpdate => "thread/metadata/update" {
|
||||
params: v2::ThreadMetadataUpdateParams,
|
||||
response: v2::ThreadMetadataUpdateResponse,
|
||||
@@ -1032,6 +1047,10 @@ server_notification_definitions! {
|
||||
ThreadClosed => "thread/closed" (v2::ThreadClosedNotification),
|
||||
SkillsChanged => "skills/changed" (v2::SkillsChangedNotification),
|
||||
ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification),
|
||||
#[experimental("thread/goal/updated")]
|
||||
ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification),
|
||||
#[experimental("thread/goal/cleared")]
|
||||
ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification),
|
||||
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
|
||||
TurnStarted => "turn/started" (v2::TurnStartedNotification),
|
||||
HookStarted => "hook/started" (v2::HookStartedNotification),
|
||||
@@ -1501,7 +1520,7 @@ mod tests {
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: cwd.clone(),
|
||||
cwd,
|
||||
instruction_sources: vec![absolute_path("/tmp/AGENTS.md")],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
@@ -1509,7 +1528,6 @@ mod tests {
|
||||
permission_profile: Some(
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
cwd.as_path(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
@@ -1556,22 +1574,7 @@ mod tests {
|
||||
"type": "dangerFullAccess"
|
||||
},
|
||||
"permissionProfile": {
|
||||
"network": {
|
||||
"enabled": true,
|
||||
},
|
||||
"fileSystem": {
|
||||
"entries": [
|
||||
{
|
||||
"path": {
|
||||
"type": "special",
|
||||
"value": {
|
||||
"kind": "root",
|
||||
},
|
||||
},
|
||||
"access": "write",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "disabled"
|
||||
},
|
||||
"reasoningEffort": null
|
||||
}
|
||||
@@ -2092,6 +2095,76 @@ mod tests {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
|
||||
assert_eq!(reason, Some("thread/realtime/start"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_goal_methods_are_marked_experimental() {
|
||||
let set_request = ClientRequest::ThreadGoalSet {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadGoalSetParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
objective: Some("ship goal mode".to_string()),
|
||||
status: Some(v2::ThreadGoalStatus::Active),
|
||||
token_budget: Some(Some(10_000)),
|
||||
},
|
||||
};
|
||||
let get_request = ClientRequest::ThreadGoalGet {
|
||||
request_id: RequestId::Integer(2),
|
||||
params: v2::ThreadGoalGetParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
},
|
||||
};
|
||||
let clear_request = ClientRequest::ThreadGoalClear {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::ThreadGoalClearParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&set_request),
|
||||
Some("thread/goal/set")
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&get_request),
|
||||
Some("thread/goal/get")
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&clear_request),
|
||||
Some("thread/goal/clear")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_goal_notifications_are_marked_experimental() {
|
||||
let goal = v2::ThreadGoal {
|
||||
thread_id: "thr_123".to_string(),
|
||||
objective: "ship goal mode".to_string(),
|
||||
status: v2::ThreadGoalStatus::Active,
|
||||
token_budget: Some(10_000),
|
||||
tokens_used: 123,
|
||||
time_used_seconds: 45,
|
||||
created_at: 1_700_000_000,
|
||||
updated_at: 1_700_000_123,
|
||||
};
|
||||
let updated = ServerNotification::ThreadGoalUpdated(v2::ThreadGoalUpdatedNotification {
|
||||
thread_id: "thr_123".to_string(),
|
||||
turn_id: None,
|
||||
goal,
|
||||
});
|
||||
let cleared = ServerNotification::ThreadGoalCleared(v2::ThreadGoalClearedNotification {
|
||||
thread_id: "thr_123".to_string(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&updated),
|
||||
Some("thread/goal/updated")
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&cleared),
|
||||
Some("thread/goal/cleared")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_realtime_started_notification_is_marked_experimental() {
|
||||
let notification =
|
||||
|
||||
@@ -3096,7 +3096,6 @@ mod tests {
|
||||
content: vec![codex_protocol::models::ContentItem::InputText {
|
||||
text: "plain text".into(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::RequestId;
|
||||
use crate::protocol::common::AuthMode;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::account::ProviderAccount;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
@@ -37,7 +38,9 @@ use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
|
||||
@@ -51,6 +54,7 @@ use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMod
|
||||
use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy as CoreNetworkSandboxPolicy;
|
||||
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
|
||||
use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
|
||||
@@ -79,7 +83,6 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType;
|
||||
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
|
||||
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
|
||||
use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame;
|
||||
use codex_protocol::protocol::RealtimeConversationVersion;
|
||||
use codex_protocol::protocol::RealtimeOutputModality;
|
||||
@@ -93,6 +96,7 @@ use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata;
|
||||
use codex_protocol::protocol::SkillScope as CoreSkillScope;
|
||||
use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
|
||||
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
|
||||
@@ -804,10 +808,6 @@ const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
const fn default_include_platform_defaults() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -1355,7 +1355,7 @@ pub struct AdditionalNetworkPermissions {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileNetworkPermissions {
|
||||
pub enabled: Option<bool>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl From<CoreNetworkPermissions> for AdditionalNetworkPermissions {
|
||||
@@ -1374,18 +1374,20 @@ impl From<AdditionalNetworkPermissions> for CoreNetworkPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreNetworkPermissions> for PermissionProfileNetworkPermissions {
|
||||
fn from(value: CoreNetworkPermissions) -> Self {
|
||||
impl From<CoreNetworkSandboxPolicy> for PermissionProfileNetworkPermissions {
|
||||
fn from(value: CoreNetworkSandboxPolicy) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
enabled: value.is_enabled(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfileNetworkPermissions> for CoreNetworkPermissions {
|
||||
impl From<PermissionProfileNetworkPermissions> for CoreNetworkSandboxPolicy {
|
||||
fn from(value: PermissionProfileNetworkPermissions) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
if value.enabled {
|
||||
Self::Enabled
|
||||
} else {
|
||||
Self::Restricted
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1533,65 +1535,111 @@ impl From<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileFileSystemPermissions {
|
||||
pub entries: Vec<FileSystemSandboxEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub glob_scan_max_depth: Option<NonZeroUsize>,
|
||||
pub enum PermissionProfileFileSystemPermissions {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Restricted {
|
||||
entries: Vec<FileSystemSandboxEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
glob_scan_max_depth: Option<NonZeroUsize>,
|
||||
},
|
||||
Unrestricted,
|
||||
}
|
||||
|
||||
impl From<CoreFileSystemPermissions> for PermissionProfileFileSystemPermissions {
|
||||
fn from(value: CoreFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
entries: value
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(FileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth: value.glob_scan_max_depth,
|
||||
impl From<CoreManagedFileSystemPermissions> for PermissionProfileFileSystemPermissions {
|
||||
fn from(value: CoreManagedFileSystemPermissions) -> Self {
|
||||
match value {
|
||||
CoreManagedFileSystemPermissions::Restricted {
|
||||
entries,
|
||||
glob_scan_max_depth,
|
||||
} => Self::Restricted {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(FileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth,
|
||||
},
|
||||
CoreManagedFileSystemPermissions::Unrestricted => Self::Unrestricted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfileFileSystemPermissions> for CoreFileSystemPermissions {
|
||||
impl From<PermissionProfileFileSystemPermissions> for CoreManagedFileSystemPermissions {
|
||||
fn from(value: PermissionProfileFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
entries: value
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(CoreFileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth: value.glob_scan_max_depth,
|
||||
match value {
|
||||
PermissionProfileFileSystemPermissions::Restricted {
|
||||
entries,
|
||||
glob_scan_max_depth,
|
||||
} => Self::Restricted {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(CoreFileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth,
|
||||
},
|
||||
PermissionProfileFileSystemPermissions::Unrestricted => Self::Unrestricted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfile {
|
||||
pub network: Option<PermissionProfileNetworkPermissions>,
|
||||
pub file_system: Option<PermissionProfileFileSystemPermissions>,
|
||||
pub enum PermissionProfile {
|
||||
/// Codex owns sandbox construction for this profile.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Managed {
|
||||
network: PermissionProfileNetworkPermissions,
|
||||
file_system: PermissionProfileFileSystemPermissions,
|
||||
},
|
||||
/// Do not apply an outer sandbox.
|
||||
Disabled,
|
||||
/// Filesystem isolation is enforced by an external caller.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
External {
|
||||
network: PermissionProfileNetworkPermissions,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CorePermissionProfile> for PermissionProfile {
|
||||
fn from(value: CorePermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(PermissionProfileNetworkPermissions::from),
|
||||
file_system: value
|
||||
.file_system
|
||||
.map(PermissionProfileFileSystemPermissions::from),
|
||||
match value {
|
||||
CorePermissionProfile::Managed {
|
||||
file_system,
|
||||
network,
|
||||
} => Self::Managed {
|
||||
network: network.into(),
|
||||
file_system: file_system.into(),
|
||||
},
|
||||
CorePermissionProfile::Disabled => Self::Disabled,
|
||||
CorePermissionProfile::External { network } => Self::External {
|
||||
network: network.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfile> for CorePermissionProfile {
|
||||
fn from(value: PermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
match value {
|
||||
PermissionProfile::Managed {
|
||||
file_system,
|
||||
network,
|
||||
} => Self::Managed {
|
||||
file_system: file_system.into(),
|
||||
network: network.into(),
|
||||
},
|
||||
PermissionProfile::Disabled => Self::Disabled,
|
||||
PermissionProfile::External { network } => Self::External {
|
||||
network: network.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1600,12 +1648,13 @@ impl From<PermissionProfile> for CorePermissionProfile {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AdditionalPermissionProfile {
|
||||
/// Partial overlay used for per-command permission requests.
|
||||
pub network: Option<AdditionalNetworkPermissions>,
|
||||
pub file_system: Option<AdditionalFileSystemPermissions>,
|
||||
}
|
||||
|
||||
impl From<CorePermissionProfile> for AdditionalPermissionProfile {
|
||||
fn from(value: CorePermissionProfile) -> Self {
|
||||
impl From<CoreAdditionalPermissionProfile> for AdditionalPermissionProfile {
|
||||
fn from(value: CoreAdditionalPermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(AdditionalNetworkPermissions::from),
|
||||
file_system: value.file_system.map(AdditionalFileSystemPermissions::from),
|
||||
@@ -1613,7 +1662,7 @@ impl From<CorePermissionProfile> for AdditionalPermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalPermissionProfile> for CorePermissionProfile {
|
||||
impl From<AdditionalPermissionProfile> for CoreAdditionalPermissionProfile {
|
||||
fn from(value: AdditionalPermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
@@ -1634,7 +1683,7 @@ pub struct GrantedPermissionProfile {
|
||||
pub file_system: Option<AdditionalFileSystemPermissions>,
|
||||
}
|
||||
|
||||
impl From<GrantedPermissionProfile> for CorePermissionProfile {
|
||||
impl From<GrantedPermissionProfile> for CoreAdditionalPermissionProfile {
|
||||
fn from(value: GrantedPermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
@@ -1666,54 +1715,7 @@ pub enum NetworkAccess {
|
||||
Enabled,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ReadOnlyAccess {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Restricted {
|
||||
#[serde(default = "default_include_platform_defaults")]
|
||||
include_platform_defaults: bool,
|
||||
#[serde(default)]
|
||||
readable_roots: Vec<AbsolutePathBuf>,
|
||||
},
|
||||
#[default]
|
||||
FullAccess,
|
||||
}
|
||||
|
||||
impl ReadOnlyAccess {
|
||||
pub fn to_core(&self) -> CoreReadOnlyAccess {
|
||||
match self {
|
||||
ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots,
|
||||
} => CoreReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: *include_platform_defaults,
|
||||
readable_roots: readable_roots.clone(),
|
||||
},
|
||||
ReadOnlyAccess::FullAccess => CoreReadOnlyAccess::FullAccess,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreReadOnlyAccess> for ReadOnlyAccess {
|
||||
fn from(value: CoreReadOnlyAccess) -> Self {
|
||||
match value {
|
||||
CoreReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots,
|
||||
} => ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
readable_roots,
|
||||
},
|
||||
CoreReadOnlyAccess::FullAccess => ReadOnlyAccess::FullAccess,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -1722,8 +1724,6 @@ pub enum SandboxPolicy {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ReadOnly {
|
||||
#[serde(default)]
|
||||
access: ReadOnlyAccess,
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
},
|
||||
@@ -1739,7 +1739,36 @@ pub enum SandboxPolicy {
|
||||
#[serde(default)]
|
||||
writable_roots: Vec<AbsolutePathBuf>,
|
||||
#[serde(default)]
|
||||
read_only_access: ReadOnlyAccess,
|
||||
network_access: bool,
|
||||
#[serde(default)]
|
||||
exclude_tmpdir_env_var: bool,
|
||||
#[serde(default)]
|
||||
exclude_slash_tmp: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
enum SandboxPolicyDeserialize {
|
||||
DangerFullAccess,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ReadOnly {
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
#[serde(default)]
|
||||
access: Option<LegacyReadOnlyAccess>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ExternalSandbox {
|
||||
#[serde(default)]
|
||||
network_access: NetworkAccess,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WorkspaceWrite {
|
||||
#[serde(default)]
|
||||
writable_roots: Vec<AbsolutePathBuf>,
|
||||
#[serde(default)]
|
||||
read_only_access: Option<LegacyReadOnlyAccess>,
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
#[serde(default)]
|
||||
@@ -1749,19 +1778,68 @@ pub enum SandboxPolicy {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
enum LegacyReadOnlyAccess {
|
||||
FullAccess,
|
||||
Restricted,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SandboxPolicy {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
match SandboxPolicyDeserialize::deserialize(deserializer)? {
|
||||
SandboxPolicyDeserialize::DangerFullAccess => Ok(SandboxPolicy::DangerFullAccess),
|
||||
SandboxPolicyDeserialize::ReadOnly {
|
||||
network_access,
|
||||
access,
|
||||
} => {
|
||||
if matches!(access, Some(LegacyReadOnlyAccess::Restricted)) {
|
||||
return Err(serde::de::Error::custom(
|
||||
"readOnly.access is no longer supported; use permissionProfile for restricted reads",
|
||||
));
|
||||
}
|
||||
Ok(SandboxPolicy::ReadOnly { network_access })
|
||||
}
|
||||
SandboxPolicyDeserialize::ExternalSandbox { network_access } => {
|
||||
Ok(SandboxPolicy::ExternalSandbox { network_access })
|
||||
}
|
||||
SandboxPolicyDeserialize::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
} => {
|
||||
if matches!(read_only_access, Some(LegacyReadOnlyAccess::Restricted)) {
|
||||
return Err(serde::de::Error::custom(
|
||||
"workspaceWrite.readOnlyAccess is no longer supported; use permissionProfile for restricted reads",
|
||||
));
|
||||
}
|
||||
Ok(SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SandboxPolicy {
|
||||
pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy {
|
||||
match self {
|
||||
SandboxPolicy::DangerFullAccess => {
|
||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||
}
|
||||
SandboxPolicy::ReadOnly {
|
||||
access,
|
||||
network_access,
|
||||
} => codex_protocol::protocol::SandboxPolicy::ReadOnly {
|
||||
access: access.to_core(),
|
||||
network_access: *network_access,
|
||||
},
|
||||
SandboxPolicy::ReadOnly { network_access } => {
|
||||
codex_protocol::protocol::SandboxPolicy::ReadOnly {
|
||||
network_access: *network_access,
|
||||
}
|
||||
}
|
||||
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||
codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||
network_access: match network_access {
|
||||
@@ -1772,13 +1850,11 @@ impl SandboxPolicy {
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
} => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: writable_roots.clone(),
|
||||
read_only_access: read_only_access.to_core(),
|
||||
network_access: *network_access,
|
||||
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp: *exclude_slash_tmp,
|
||||
@@ -1793,13 +1869,9 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
|
||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess => {
|
||||
SandboxPolicy::DangerFullAccess
|
||||
}
|
||||
codex_protocol::protocol::SandboxPolicy::ReadOnly {
|
||||
access,
|
||||
network_access,
|
||||
} => SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::from(access),
|
||||
network_access,
|
||||
},
|
||||
codex_protocol::protocol::SandboxPolicy::ReadOnly { network_access } => {
|
||||
SandboxPolicy::ReadOnly { network_access }
|
||||
}
|
||||
codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => {
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: match network_access {
|
||||
@@ -1810,13 +1882,11 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
|
||||
}
|
||||
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
} => SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access: ReadOnlyAccess::from(read_only_access),
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
@@ -2015,6 +2085,20 @@ pub enum Account {
|
||||
#[serde(rename = "chatgpt", rename_all = "camelCase")]
|
||||
#[ts(rename = "chatgpt", rename_all = "camelCase")]
|
||||
Chatgpt { email: String, plan_type: PlanType },
|
||||
|
||||
#[serde(rename = "amazonBedrock", rename_all = "camelCase")]
|
||||
#[ts(rename = "amazonBedrock", rename_all = "camelCase")]
|
||||
AmazonBedrock {},
|
||||
}
|
||||
|
||||
impl From<ProviderAccount> for Account {
|
||||
fn from(account: ProviderAccount) -> Self {
|
||||
match account {
|
||||
ProviderAccount::ApiKey => Self::ApiKey {},
|
||||
ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt { email, plan_type },
|
||||
ProviderAccount::AmazonBedrock => Self::AmazonBedrock {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
@@ -3319,6 +3403,15 @@ pub struct ThreadStartParams {
|
||||
pub ephemeral: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_start_source: Option<ThreadStartSource>,
|
||||
/// Optional sticky environments for this thread.
|
||||
///
|
||||
/// Omitted selects the default environment when environment access is
|
||||
/// enabled. Empty disables environment access for turns that do not
|
||||
/// provide a turn override. Non-empty selects the first environment as the
|
||||
/// current turn environment.
|
||||
#[experimental("thread/start.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
#[experimental("thread/start.dynamicTools")]
|
||||
#[ts(optional = nullable)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
@@ -3376,9 +3469,7 @@ pub struct ThreadStartResponse {
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
/// Canonical active permissions view for this thread.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -3482,9 +3573,7 @@ pub struct ThreadResumeResponse {
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
/// Canonical active permissions view for this thread.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -3579,9 +3668,7 @@ pub struct ThreadForkResponse {
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
/// Canonical active permissions view for this thread.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -3682,6 +3769,103 @@ pub struct ThreadUnarchiveParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSetNameResponse {}
|
||||
|
||||
v2_enum_from_core! {
|
||||
pub enum ThreadGoalStatus from CoreThreadGoalStatus {
|
||||
Active,
|
||||
Paused,
|
||||
BudgetLimited,
|
||||
Complete,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoal {
|
||||
pub thread_id: String,
|
||||
pub objective: String,
|
||||
pub status: ThreadGoalStatus,
|
||||
#[ts(type = "number | null")]
|
||||
pub token_budget: Option<i64>,
|
||||
#[ts(type = "number")]
|
||||
pub tokens_used: i64,
|
||||
#[ts(type = "number")]
|
||||
pub time_used_seconds: i64,
|
||||
#[ts(type = "number")]
|
||||
pub created_at: i64,
|
||||
#[ts(type = "number")]
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl From<codex_protocol::protocol::ThreadGoal> for ThreadGoal {
|
||||
fn from(value: codex_protocol::protocol::ThreadGoal) -> Self {
|
||||
Self {
|
||||
thread_id: value.thread_id.to_string(),
|
||||
objective: value.objective,
|
||||
status: value.status.into(),
|
||||
token_budget: value.token_budget,
|
||||
tokens_used: value.tokens_used,
|
||||
time_used_seconds: value.time_used_seconds,
|
||||
created_at: value.created_at,
|
||||
updated_at: value.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalSetParams {
|
||||
pub thread_id: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub objective: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub status: Option<ThreadGoalStatus>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "number | null")]
|
||||
pub token_budget: Option<Option<i64>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalSetResponse {
|
||||
pub goal: ThreadGoal,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalGetParams {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalGetResponse {
|
||||
pub goal: Option<ThreadGoal>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalClearParams {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalClearResponse {
|
||||
pub cleared: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4999,7 +5183,11 @@ pub struct TurnStartParams {
|
||||
#[experimental("turn/start.responsesapiClientMetadata")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Optional turn-scoped environment selections.
|
||||
/// Optional turn-scoped environments.
|
||||
///
|
||||
/// Omitted uses the thread sticky environments. Empty disables
|
||||
/// environment access for this turn. Non-empty selects the first
|
||||
/// environment as the current turn environment for this turn.
|
||||
#[experimental("turn/start.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
@@ -6201,6 +6389,22 @@ pub struct ThreadNameUpdatedNotification {
|
||||
pub thread_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalUpdatedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: Option<String>,
|
||||
pub goal: ThreadGoal,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadGoalClearedNotification {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -7525,7 +7729,6 @@ mod tests {
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
|
||||
use codex_protocol::user_input::UserInput as CoreUserInput;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
@@ -7889,7 +8092,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn permission_profile_file_system_permissions_preserves_glob_scan_depth() {
|
||||
let core_permissions = CoreFileSystemPermissions {
|
||||
let core_permissions = CoreManagedFileSystemPermissions::Restricted {
|
||||
entries: vec![CoreFileSystemSandboxEntry {
|
||||
path: CoreFileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
@@ -7903,7 +8106,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
permissions,
|
||||
PermissionProfileFileSystemPermissions {
|
||||
PermissionProfileFileSystemPermissions::Restricted {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
@@ -7914,7 +8117,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
CoreFileSystemPermissions::from(permissions),
|
||||
CoreManagedFileSystemPermissions::from(permissions),
|
||||
core_permissions
|
||||
);
|
||||
}
|
||||
@@ -7922,6 +8125,7 @@ mod tests {
|
||||
#[test]
|
||||
fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() {
|
||||
serde_json::from_value::<PermissionProfileFileSystemPermissions>(json!({
|
||||
"type": "restricted",
|
||||
"entries": [],
|
||||
"globScanMaxDepth": 0,
|
||||
}))
|
||||
@@ -7975,8 +8179,8 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
CorePermissionProfile::from(response.permissions),
|
||||
CorePermissionProfile {
|
||||
CoreAdditionalPermissionProfile::from(response.permissions),
|
||||
CoreAdditionalPermissionProfile {
|
||||
network: Some(CoreNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
@@ -8730,13 +8934,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_read_only_access() {
|
||||
let readable_root = test_absolute_path();
|
||||
fn sandbox_policy_round_trips_read_only_network_access() {
|
||||
let v2_policy = SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: vec![readable_root.clone()],
|
||||
},
|
||||
network_access: true,
|
||||
};
|
||||
|
||||
@@ -8744,10 +8943,6 @@ mod tests {
|
||||
assert_eq!(
|
||||
core_policy,
|
||||
codex_protocol::protocol::SandboxPolicy::ReadOnly {
|
||||
access: CoreReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: vec![readable_root],
|
||||
},
|
||||
network_access: true,
|
||||
}
|
||||
);
|
||||
@@ -9371,14 +9566,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_workspace_write_read_only_access() {
|
||||
let readable_root = test_absolute_path();
|
||||
fn sandbox_policy_round_trips_workspace_write_access() {
|
||||
let v2_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: vec![readable_root.clone()],
|
||||
},
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
@@ -9389,10 +9579,6 @@ mod tests {
|
||||
core_policy,
|
||||
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: CoreReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: vec![readable_root],
|
||||
},
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
@@ -9404,40 +9590,78 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_deserializes_legacy_read_only_without_access_field() {
|
||||
let policy: SandboxPolicy = serde_json::from_value(json!({
|
||||
"type": "readOnly"
|
||||
fn sandbox_policy_deserializes_legacy_read_only_full_access_field() {
|
||||
let policy = serde_json::from_value::<SandboxPolicy>(json!({
|
||||
"type": "readOnly",
|
||||
"access": {
|
||||
"type": "fullAccess"
|
||||
},
|
||||
"networkAccess": true
|
||||
}))
|
||||
.expect("read-only policy should deserialize");
|
||||
.expect("read-only policy should ignore legacy fullAccess field");
|
||||
assert_eq!(
|
||||
policy,
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
network_access: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_deserializes_legacy_workspace_write_without_read_only_access_field() {
|
||||
let policy: SandboxPolicy = serde_json::from_value(json!({
|
||||
fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() {
|
||||
let writable_root = absolute_path("/workspace");
|
||||
let policy = serde_json::from_value::<SandboxPolicy>(json!({
|
||||
"type": "workspaceWrite",
|
||||
"writableRoots": [writable_root],
|
||||
"readOnlyAccess": {
|
||||
"type": "fullAccess"
|
||||
},
|
||||
"networkAccess": true,
|
||||
"excludeTmpdirEnvVar": true,
|
||||
"excludeSlashTmp": true
|
||||
}))
|
||||
.expect("workspace-write policy should ignore legacy fullAccess field");
|
||||
assert_eq!(
|
||||
policy,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![absolute_path("/workspace")],
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() {
|
||||
let err = serde_json::from_value::<SandboxPolicy>(json!({
|
||||
"type": "readOnly",
|
||||
"access": {
|
||||
"type": "restricted",
|
||||
"includePlatformDefaults": false,
|
||||
"readableRoots": []
|
||||
}
|
||||
}))
|
||||
.expect_err("read-only policy should reject removed restricted access field");
|
||||
assert!(err.to_string().contains("readOnly.access"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_rejects_legacy_workspace_write_restricted_read_access_field() {
|
||||
let err = serde_json::from_value::<SandboxPolicy>(json!({
|
||||
"type": "workspaceWrite",
|
||||
"writableRoots": [],
|
||||
"readOnlyAccess": {
|
||||
"type": "restricted",
|
||||
"includePlatformDefaults": false,
|
||||
"readableRoots": []
|
||||
},
|
||||
"networkAccess": false,
|
||||
"excludeTmpdirEnvVar": false,
|
||||
"excludeSlashTmp": false
|
||||
}))
|
||||
.expect("workspace-write policy should deserialize");
|
||||
assert_eq!(
|
||||
policy,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
}
|
||||
);
|
||||
.expect_err("workspace-write policy should reject removed restricted readOnlyAccess field");
|
||||
assert!(err.to_string().contains("workspaceWrite.readOnlyAccess"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -48,7 +48,6 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::ReadOnlyAccess;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
@@ -743,7 +742,6 @@ async fn trigger_zsh_fork_multi_cmd_approval(
|
||||
};
|
||||
turn_params.approval_policy = Some(AskForApproval::OnRequest);
|
||||
turn_params.sandbox_policy = Some(SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
});
|
||||
|
||||
@@ -885,7 +883,6 @@ async fn trigger_cmd_approval(
|
||||
experimental_api: true,
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
sandbox_policy: Some(SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
}),
|
||||
dynamic_tools,
|
||||
@@ -912,7 +909,6 @@ async fn trigger_patch_approval(
|
||||
experimental_api: true,
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
sandbox_policy: Some(SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
}),
|
||||
dynamic_tools,
|
||||
|
||||
@@ -5,7 +5,13 @@ codex_rust_crate(
|
||||
crate_name = "codex_app_server",
|
||||
integration_test_timeout = "long",
|
||||
test_shard_counts = {
|
||||
"app-server-all-test": 8,
|
||||
# Note app-server-all-test has a large number of integration tests, so
|
||||
# even a single shard can be quite slow. When there is a legitimate
|
||||
# test failure in a shard, it will still get run 3x in total, which
|
||||
# can cause us to exhaust our CI timeout if the shard happens to run
|
||||
# long. Using a higher shard count for app-server-all-test should help
|
||||
# mitigate this risk.
|
||||
"app-server-all-test": 16,
|
||||
"app-server-unit-tests": 8,
|
||||
},
|
||||
test_tags = ["no-sandbox"],
|
||||
|
||||
@@ -30,6 +30,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"ws",
|
||||
] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
@@ -48,6 +49,7 @@ codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
|
||||
@@ -25,7 +25,7 @@ Supported transports:
|
||||
|
||||
- stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL)
|
||||
- websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**)
|
||||
- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket frames over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path without HTTP upgrade
|
||||
- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket connections over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path, using the standard HTTP Upgrade handshake
|
||||
- off (`--listen off`): do not expose a local transport
|
||||
|
||||
When running with `--listen ws://IP:PORT`, the same listener also serves basic HTTP health probes:
|
||||
@@ -39,7 +39,7 @@ Websocket transport is currently experimental and unsupported. Do not rely on it
|
||||
The unix socket transport is intended for local app-server control-plane clients. `codex app-server proxy`
|
||||
opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-server-control.sock`
|
||||
by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout.
|
||||
The socket uses websocket framing directly over the Unix socket, without an HTTP upgrade handshake.
|
||||
The proxied stream carries the websocket HTTP Upgrade handshake followed by websocket frames.
|
||||
|
||||
Security note:
|
||||
|
||||
@@ -142,7 +142,7 @@ Example with notification opt-out:
|
||||
|
||||
## API Overview
|
||||
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer `permissionProfile`; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissionProfile`.
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer `permissionProfile`; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissionProfile`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Pass `excludeTurns: true` when the client plans to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
@@ -152,6 +152,11 @@ Example with notification opt-out:
|
||||
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
|
||||
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
||||
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
||||
- `thread/goal/set` — create, replace, or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`. Supplying a new `objective` replaces the goal and resets usage accounting. Supplying the current non-terminal objective or omitting `objective` updates the existing goal’s status and/or token budget while preserving usage.
|
||||
- `thread/goal/get` — fetch the current persisted goal for a materialized thread; returns `goal: null` when no goal exists.
|
||||
- `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes.
|
||||
- `thread/goal/updated` — notification emitted whenever a thread goal changes; includes the full current goal.
|
||||
- `thread/goal/cleared` — notification emitted whenever a thread goal is removed.
|
||||
- `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`).
|
||||
- `thread/archive` — move a thread’s rollout file into the archived directory and attempt to move any spawned descendant thread rollout files; returns `{}` on success and emits `thread/archived` for each archived thread.
|
||||
- `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server keeps the thread loaded and unloads it only after it has had no subscribers and no thread activity for 30 minutes, then emits `thread/closed`.
|
||||
@@ -470,6 +475,70 @@ Experimental: use `memory/reset` to clear local memory artifacts and sqlite-back
|
||||
{ "id": 27, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Set and update a thread goal
|
||||
|
||||
Use `thread/goal/set` with an `objective` to create or replace the current goal for a materialized thread. Supplying a new objective resets `tokensUsed`, `timeUsedSeconds`, and `createdAt`. Supplying the current non-terminal objective, or omitting `objective`, updates the existing goal’s status or token budget while preserving usage history. Clients can set `budgetLimited` when they stop because a token budget is exhausted or nearly exhausted; the system also sets it when accounting crosses a configured token budget.
|
||||
|
||||
```json
|
||||
{ "method": "thread/goal/set", "id": 27, "params": {
|
||||
"threadId": "thr_123",
|
||||
"objective": "Keep improving the benchmark until p95 latency is under 120ms",
|
||||
"tokenBudget": 200000
|
||||
} }
|
||||
{ "id": 27, "result": { "goal": {
|
||||
"threadId": "thr_123",
|
||||
"objective": "Keep improving the benchmark until p95 latency is under 120ms",
|
||||
"status": "active",
|
||||
"tokenBudget": 200000,
|
||||
"tokensUsed": 0,
|
||||
"timeUsedSeconds": 0,
|
||||
"createdAt": 1776272400,
|
||||
"updatedAt": 1776272400
|
||||
} } }
|
||||
{ "method": "thread/goal/updated", "params": { "threadId": "thr_123", "goal": {
|
||||
"threadId": "thr_123",
|
||||
"objective": "Keep improving the benchmark until p95 latency is under 120ms",
|
||||
"status": "active",
|
||||
"tokenBudget": 200000,
|
||||
"tokensUsed": 0,
|
||||
"timeUsedSeconds": 0,
|
||||
"createdAt": 1776272400,
|
||||
"updatedAt": 1776272400
|
||||
} } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "method": "thread/goal/set", "id": 28, "params": {
|
||||
"threadId": "thr_123",
|
||||
"status": "paused"
|
||||
} }
|
||||
{ "id": 28, "result": { "goal": {
|
||||
"threadId": "thr_123",
|
||||
"objective": "Keep improving the benchmark until p95 latency is under 120ms",
|
||||
"status": "paused",
|
||||
"tokenBudget": 200000,
|
||||
"tokensUsed": 10000,
|
||||
"timeUsedSeconds": 60,
|
||||
"createdAt": 1776272400,
|
||||
"updatedAt": 1776272460
|
||||
} } }
|
||||
```
|
||||
|
||||
Use `thread/goal/get` to read the current goal without changing it.
|
||||
|
||||
```json
|
||||
{ "method": "thread/goal/get", "id": 29, "params": { "threadId": "thr_123" } }
|
||||
{ "id": 29, "result": { "goal": null } }
|
||||
```
|
||||
|
||||
Use `thread/goal/clear` to remove the current goal.
|
||||
|
||||
```json
|
||||
{ "method": "thread/goal/clear", "id": 30, "params": { "threadId": "thr_123" } }
|
||||
{ "id": 30, "result": { "cleared": true } }
|
||||
{ "method": "thread/goal/cleared", "params": { "threadId": "thr_123" } }
|
||||
```
|
||||
|
||||
### Example: Archive a thread
|
||||
|
||||
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory and attempt to move any spawned descendant thread rollouts.
|
||||
@@ -541,7 +610,7 @@ Turns attach user input (text or images) to a thread and trigger Codex generatio
|
||||
- `{"type":"image","url":"https://…png"}`
|
||||
- `{"type":"localImage","path":"/tmp/screenshot.png"}`
|
||||
|
||||
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn.
|
||||
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn. Experimental `environments` is turn-scoped: omit it to inherit the thread's sticky environments, pass `[]` to run the turn with no environments, or pass explicit environment ids to override the sticky selection for this turn only.
|
||||
|
||||
`approvalsReviewer` accepts:
|
||||
|
||||
@@ -837,7 +906,8 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin
|
||||
"env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names
|
||||
"size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true
|
||||
"permissionProfile": { // optional; defaults to user config
|
||||
"fileSystem": { "entries": [
|
||||
"type": "managed",
|
||||
"fileSystem": { "type": "restricted", "entries": [
|
||||
{ "path": { "type": "special", "value": { "kind": "root" } }, "access": "read" },
|
||||
{ "path": { "type": "special", "value": { "kind": "current_working_directory" } }, "access": "write" }
|
||||
] },
|
||||
@@ -1232,7 +1302,7 @@ If the session approval policy uses `Granular` with `request_permissions: false`
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `js_repl`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
|
||||
When a dynamic tool is invoked during a turn, the server sends an `item/tool/call` JSON-RPC request to the client:
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::SkillsChangedNotification;
|
||||
use codex_app_server_protocol::TerminalInteractionNotification;
|
||||
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadNameUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeClosedNotification;
|
||||
@@ -122,7 +123,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -222,6 +223,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
EventMsg::TurnComplete(turn_complete_event) => {
|
||||
// All per-thread requests are bound to a turn, so abort them.
|
||||
outgoing.abort_pending_server_requests().await;
|
||||
respond_to_pending_interrupts(&thread_state, &outgoing, /*abort_reason*/ None).await;
|
||||
let turn_failed = thread_state.lock().await.turn_summary.last_error.is_some();
|
||||
thread_watch_manager
|
||||
.note_turn_completed(&conversation_id.to_string(), turn_failed)
|
||||
@@ -1846,26 +1848,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
EventMsg::TurnAborted(turn_aborted_event) => {
|
||||
// All per-thread requests are bound to a turn, so abort them.
|
||||
outgoing.abort_pending_server_requests().await;
|
||||
let pending = {
|
||||
let mut state = thread_state.lock().await;
|
||||
std::mem::take(&mut state.pending_interrupts)
|
||||
};
|
||||
if !pending.is_empty() {
|
||||
for (rid, ver) in pending {
|
||||
match ver {
|
||||
ApiVersion::V1 => {
|
||||
let response = InterruptConversationResponse {
|
||||
abort_reason: turn_aborted_event.reason.clone(),
|
||||
};
|
||||
outgoing.send_response(rid, response).await;
|
||||
}
|
||||
ApiVersion::V2 => {
|
||||
let response = TurnInterruptResponse {};
|
||||
outgoing.send_response(rid, response).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
respond_to_pending_interrupts(
|
||||
&thread_state,
|
||||
&outgoing,
|
||||
Some(turn_aborted_event.reason.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
thread_watch_manager
|
||||
.note_turn_interrupted(&conversation_id.to_string())
|
||||
@@ -1967,6 +1955,20 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::ThreadGoalUpdated(thread_goal_event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let notification = ThreadGoalUpdatedNotification {
|
||||
thread_id: thread_goal_event.thread_id.to_string(),
|
||||
turn_id: thread_goal_event.turn_id,
|
||||
goal: thread_goal_event.goal.clone().into(),
|
||||
};
|
||||
outgoing
|
||||
.send_global_server_notification(ServerNotification::ThreadGoalUpdated(
|
||||
notification,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::TurnDiff(turn_diff_event) => {
|
||||
handle_turn_diff(
|
||||
conversation_id,
|
||||
@@ -2342,6 +2344,33 @@ async fn handle_thread_rollback_failed(
|
||||
}
|
||||
}
|
||||
|
||||
async fn respond_to_pending_interrupts(
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
abort_reason: Option<codex_protocol::protocol::TurnAbortReason>,
|
||||
) {
|
||||
let pending = {
|
||||
let mut state = thread_state.lock().await;
|
||||
std::mem::take(&mut state.pending_interrupts)
|
||||
};
|
||||
|
||||
for (rid, ver) in pending {
|
||||
match ver {
|
||||
ApiVersion::V1 => {
|
||||
let Some(abort_reason) = abort_reason.clone() else {
|
||||
debug_assert!(false, "v1 interrupts only resolve from TurnAborted");
|
||||
continue;
|
||||
};
|
||||
let response = InterruptConversationResponse { abort_reason };
|
||||
outgoing.send_response(rid, response).await;
|
||||
}
|
||||
ApiVersion::V2 => {
|
||||
outgoing.send_response(rid, TurnInterruptResponse {}).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_token_count_event(
|
||||
conversation_id: ThreadId,
|
||||
turn_id: String,
|
||||
@@ -2719,7 +2748,7 @@ fn request_permissions_response_from_client_result(
|
||||
strict_auto_review: false,
|
||||
});
|
||||
}
|
||||
let granted_permissions: CorePermissionProfile = response.permissions.into();
|
||||
let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.into();
|
||||
let permissions = if granted_permissions.is_empty() {
|
||||
CoreRequestPermissionProfile::default()
|
||||
} else {
|
||||
@@ -4192,17 +4221,19 @@ mod tests {
|
||||
let thread_state = new_thread_state();
|
||||
{
|
||||
let mut state = thread_state.lock().await;
|
||||
state.track_current_turn_event(&EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
state.track_current_turn_event(
|
||||
&event_turn_id,
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: event_turn_id.clone(),
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
},
|
||||
));
|
||||
state.track_current_turn_event(&EventMsg::TurnComplete(turn_complete_event(
|
||||
}),
|
||||
);
|
||||
state.track_current_turn_event(
|
||||
&event_turn_id,
|
||||
)));
|
||||
&EventMsg::TurnComplete(turn_complete_event(&event_turn_id)),
|
||||
);
|
||||
}
|
||||
|
||||
handle_turn_complete(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(super) async fn plugin_list(
|
||||
@@ -30,8 +31,24 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
plugins_manager.maybe_start_non_curated_plugin_cache_refresh(&roots);
|
||||
let auth = self.auth_manager.auth().await;
|
||||
if !self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
PluginListResponse {
|
||||
marketplaces: Vec::new(),
|
||||
marketplace_load_errors: Vec::new(),
|
||||
featured_plugin_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
plugins_manager.maybe_start_non_curated_plugin_cache_refresh(&roots);
|
||||
|
||||
let config_for_marketplace_listing = config.clone();
|
||||
let plugins_manager_for_marketplace_listing = plugins_manager.clone();
|
||||
@@ -358,17 +375,7 @@ impl CodexMessageProcessor {
|
||||
let marketplace_path = match (marketplace_path, remote_marketplace_name) {
|
||||
(Some(marketplace_path), None) => marketplace_path,
|
||||
(None, Some(remote_marketplace_name)) => {
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"remote plugin install is not supported yet for marketplace {remote_marketplace_name}"
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
self.remote_plugin_install(request_id, remote_marketplace_name, plugin_name)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
@@ -387,6 +394,26 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf);
|
||||
let config = match self.load_latest_config(config_cwd.clone()).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let auth = self.auth_manager.auth().await;
|
||||
|
||||
if !self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"Codex plugins are disabled for this workspace".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let request = PluginInstallRequest {
|
||||
@@ -404,7 +431,7 @@ impl CodexMessageProcessor {
|
||||
warn!(
|
||||
"failed to reload config after plugin install, using current config: {err:?}"
|
||||
);
|
||||
self.config.as_ref().clone()
|
||||
config
|
||||
}
|
||||
};
|
||||
|
||||
@@ -426,66 +453,14 @@ impl CodexMessageProcessor {
|
||||
|
||||
let plugin_apps = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let apps_needing_auth = if plugin_apps.is_empty()
|
||||
|| !config.features.apps_enabled_for_auth(
|
||||
let apps_needing_auth = self
|
||||
.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth),
|
||||
) {
|
||||
Vec::new()
|
||||
} else {
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||
connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true),
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&config, /*force_refetch*/ true, &environment_manager
|
||||
),
|
||||
);
|
||||
|
||||
let all_connectors = match all_connectors_result {
|
||||
Ok(connectors) => connectors,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"failed to load app metadata after plugin install: {err:#}"
|
||||
);
|
||||
connectors::list_cached_all_connectors(&config)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
let all_connectors =
|
||||
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps);
|
||||
let (accessible_connectors, codex_apps_ready) =
|
||||
match accessible_connectors_result {
|
||||
Ok(status) => (status.connectors, status.codex_apps_ready),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"failed to load accessible apps after plugin install: {err:#}"
|
||||
);
|
||||
(
|
||||
connectors::list_cached_accessible_connectors_from_mcp_tools(
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
if !codex_apps_ready {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"codex_apps MCP not ready after plugin install; skipping appsNeedingAuth check"
|
||||
);
|
||||
}
|
||||
|
||||
plugin_app_helpers::plugin_apps_needing_auth(
|
||||
&all_connectors,
|
||||
&accessible_connectors,
|
||||
&result.plugin_id.as_key(),
|
||||
&plugin_apps,
|
||||
codex_apps_ready,
|
||||
)
|
||||
};
|
||||
.await;
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
@@ -542,6 +517,193 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn remote_plugin_install(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
remote_marketplace_name: String,
|
||||
plugin_name: String,
|
||||
) {
|
||||
let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
if !config.features.enabled(Feature::Plugins)
|
||||
|| !config.features.enabled(Feature::RemotePlugin)
|
||||
{
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"remote plugin install is not enabled for marketplace {remote_marketplace_name}"
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if plugin_name.is_empty()
|
||||
|| !plugin_name
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '~')
|
||||
{
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"invalid remote plugin id: only ASCII letters, digits, `_`, `-`, and `~` are allowed"
|
||||
.to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let remote_detail = match codex_core_plugins::remote::fetch_remote_plugin_detail(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&remote_marketplace_name,
|
||||
&plugin_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(remote_detail) => remote_detail,
|
||||
Err(err) => {
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
remote_plugin_catalog_error_to_jsonrpc(
|
||||
err,
|
||||
"read remote plugin details before install",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
if remote_detail.summary.install_policy == PluginInstallPolicy::NotAvailable {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("remote plugin {plugin_name} is not available for install"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = codex_core_plugins::remote::install_remote_plugin(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&remote_marketplace_name,
|
||||
&plugin_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "install remote plugin"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
self.clear_plugin_related_caches();
|
||||
|
||||
let plugin_apps = remote_detail
|
||||
.app_ids
|
||||
.into_iter()
|
||||
.map(codex_core::plugins::AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let apps_needing_auth = self
|
||||
.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth),
|
||||
&plugin_name,
|
||||
&plugin_apps,
|
||||
)
|
||||
.await;
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
PluginInstallResponse {
|
||||
auth_policy: remote_detail.summary.auth_policy,
|
||||
apps_needing_auth,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn plugin_apps_needing_auth_for_install(
|
||||
&self,
|
||||
config: &Config,
|
||||
is_chatgpt_auth: bool,
|
||||
plugin_id: &str,
|
||||
plugin_apps: &[codex_core::plugins::AppConnectorId],
|
||||
) -> Vec<AppSummary> {
|
||||
if plugin_apps.is_empty() || !config.features.apps_enabled_for_auth(is_chatgpt_auth) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||
connectors::list_all_connectors_with_options(config, /*force_refetch*/ true),
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config,
|
||||
/*force_refetch*/ true,
|
||||
&environment_manager
|
||||
),
|
||||
);
|
||||
|
||||
let all_connectors = match all_connectors_result {
|
||||
Ok(connectors) => connectors,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = plugin_id,
|
||||
"failed to load app metadata after plugin install: {err:#}"
|
||||
);
|
||||
connectors::list_cached_all_connectors(config)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
let all_connectors = connectors::connectors_for_plugin_apps(all_connectors, plugin_apps);
|
||||
let (accessible_connectors, codex_apps_ready) = match accessible_connectors_result {
|
||||
Ok(status) => (status.connectors, status.codex_apps_ready),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = plugin_id,
|
||||
"failed to load accessible apps after plugin install: {err:#}"
|
||||
);
|
||||
(
|
||||
connectors::list_cached_accessible_connectors_from_mcp_tools(config)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
if !codex_apps_ready {
|
||||
warn!(
|
||||
plugin = plugin_id,
|
||||
"codex_apps MCP not ready after plugin install; skipping appsNeedingAuth check"
|
||||
);
|
||||
}
|
||||
|
||||
plugin_app_helpers::plugin_apps_needing_auth(
|
||||
&all_connectors,
|
||||
&accessible_connectors,
|
||||
plugin_apps,
|
||||
codex_apps_ready,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn plugin_uninstall(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -686,7 +848,9 @@ fn remote_plugin_catalog_error_to_jsonrpc(
|
||||
RemotePluginCatalogError::AuthToken(_)
|
||||
| RemotePluginCatalogError::Request { .. }
|
||||
| RemotePluginCatalogError::UnexpectedStatus { .. }
|
||||
| RemotePluginCatalogError::Decode { .. } => JSONRPCErrorError {
|
||||
| RemotePluginCatalogError::Decode { .. }
|
||||
| RemotePluginCatalogError::UnexpectedPluginId { .. }
|
||||
| RemotePluginCatalogError::UnexpectedEnabledState { .. } => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("{context}: {err}"),
|
||||
data: None,
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
use super::*;
|
||||
use codex_protocol::protocol::validate_thread_goal_objective;
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(super) async fn thread_goal_set(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadGoalSetParams,
|
||||
) {
|
||||
if !self.config.features.enabled(Feature::Goals) {
|
||||
self.send_invalid_request_error(request_id, "goals feature is disabled".to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let thread_id = match parse_thread_id_for_request(params.thread_id.as_str()) {
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let state_db = match self.state_db_for_materialized_thread(thread_id).await {
|
||||
Ok(state_db) => state_db,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let running_thread = self.thread_manager.get_thread(thread_id).await.ok();
|
||||
let rollout_path = match running_thread.as_ref() {
|
||||
Some(thread) => match thread.rollout_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("ephemeral thread does not support goals: {thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
match find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("thread not found: {thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
reconcile_rollout(
|
||||
Some(&state_db),
|
||||
rollout_path.as_path(),
|
||||
self.config.model_provider_id.as_str(),
|
||||
/*builder*/ None,
|
||||
&[],
|
||||
/*archived_only*/ None,
|
||||
/*new_thread_memory_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let listener_command_tx = {
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
let thread_state = thread_state.lock().await;
|
||||
thread_state.listener_command_tx()
|
||||
};
|
||||
let status = params.status.map(thread_goal_status_to_state);
|
||||
let objective = params.objective.as_deref().map(str::trim);
|
||||
|
||||
if let Some(objective) = objective {
|
||||
if let Err(message) = validate_thread_goal_objective(objective) {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
if let Err(message) = validate_goal_budget(params.token_budget.flatten()) {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
} else if let Some(token_budget) = params.token_budget
|
||||
&& let Err(message) = validate_goal_budget(token_budget)
|
||||
{
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(thread) = running_thread.as_ref() {
|
||||
thread.prepare_external_goal_mutation().await;
|
||||
}
|
||||
|
||||
let goal = if let Some(objective) = objective {
|
||||
match state_db.get_thread_goal(thread_id).await {
|
||||
Ok(goal) => {
|
||||
if let Some(goal) = goal.as_ref().filter(|goal| {
|
||||
goal.objective == objective
|
||||
&& goal.status != codex_state::ThreadGoalStatus::Complete
|
||||
}) {
|
||||
state_db
|
||||
.update_thread_goal(
|
||||
thread_id,
|
||||
codex_state::ThreadGoalUpdate {
|
||||
status,
|
||||
token_budget: params.token_budget,
|
||||
expected_goal_id: Some(goal.goal_id.clone()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.and_then(|goal| {
|
||||
goal.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"cannot update goal for thread {thread_id}: no goal exists"
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
state_db
|
||||
.replace_thread_goal(
|
||||
thread_id,
|
||||
objective,
|
||||
status.unwrap_or(codex_state::ThreadGoalStatus::Active),
|
||||
params.token_budget.flatten(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
state_db
|
||||
.update_thread_goal(
|
||||
thread_id,
|
||||
codex_state::ThreadGoalUpdate {
|
||||
status,
|
||||
token_budget: params.token_budget,
|
||||
expected_goal_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.and_then(|goal| {
|
||||
goal.ok_or_else(|| {
|
||||
anyhow::anyhow!("cannot update goal for thread {thread_id}: no goal exists")
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let goal = match goal {
|
||||
Ok(goal) => goal,
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let goal_status = goal.status;
|
||||
let goal = api_thread_goal_from_state(goal);
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id.clone(),
|
||||
ThreadGoalSetResponse { goal: goal.clone() },
|
||||
)
|
||||
.await;
|
||||
self.emit_thread_goal_updated_ordered(thread_id, goal, listener_command_tx)
|
||||
.await;
|
||||
if let Some(thread) = running_thread.as_ref() {
|
||||
thread.apply_external_goal_set(goal_status).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn thread_goal_get(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadGoalGetParams,
|
||||
) {
|
||||
if !self.config.features.enabled(Feature::Goals) {
|
||||
self.send_invalid_request_error(request_id, "goals feature is disabled".to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let thread_id = match parse_thread_id_for_request(params.thread_id.as_str()) {
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let state_db = match self.state_db_for_materialized_thread(thread_id).await {
|
||||
Ok(state_db) => state_db,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let goal = match state_db.get_thread_goal(thread_id).await {
|
||||
Ok(goal) => goal.map(api_thread_goal_from_state),
|
||||
Err(err) => {
|
||||
self.send_internal_error(request_id, format!("failed to read thread goal: {err}"))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadGoalGetResponse { goal })
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn thread_goal_clear(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadGoalClearParams,
|
||||
) {
|
||||
if !self.config.features.enabled(Feature::Goals) {
|
||||
self.send_invalid_request_error(request_id, "goals feature is disabled".to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let thread_id = match parse_thread_id_for_request(params.thread_id.as_str()) {
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let state_db = match self.state_db_for_materialized_thread(thread_id).await {
|
||||
Ok(state_db) => state_db,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let running_thread = self.thread_manager.get_thread(thread_id).await.ok();
|
||||
let rollout_path = match running_thread.as_ref() {
|
||||
Some(thread) => match thread.rollout_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("ephemeral thread does not support goals: {thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
match find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("thread not found: {thread_id}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to locate thread id {thread_id}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
reconcile_rollout(
|
||||
Some(&state_db),
|
||||
rollout_path.as_path(),
|
||||
self.config.model_provider_id.as_str(),
|
||||
/*builder*/ None,
|
||||
&[],
|
||||
/*archived_only*/ None,
|
||||
/*new_thread_memory_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(thread) = running_thread.as_ref() {
|
||||
thread.prepare_external_goal_mutation().await;
|
||||
}
|
||||
|
||||
let listener_command_tx = {
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
let thread_state = thread_state.lock().await;
|
||||
thread_state.listener_command_tx()
|
||||
};
|
||||
let cleared = match state_db.delete_thread_goal(thread_id).await {
|
||||
Ok(cleared) => cleared,
|
||||
Err(err) => {
|
||||
self.send_internal_error(request_id, format!("failed to clear thread goal: {err}"))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if cleared && let Some(thread) = running_thread.as_ref() {
|
||||
thread.apply_external_goal_clear().await;
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadGoalClearResponse { cleared })
|
||||
.await;
|
||||
if cleared {
|
||||
self.emit_thread_goal_cleared_ordered(thread_id, listener_command_tx)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn state_db_for_materialized_thread(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<StateDbHandle, JSONRPCErrorError> {
|
||||
if let Ok(thread) = self.thread_manager.get_thread(thread_id).await {
|
||||
if thread.rollout_path().is_none() {
|
||||
return Err(invalid_request(format!(
|
||||
"ephemeral thread does not support goals: {thread_id}"
|
||||
)));
|
||||
}
|
||||
if let Some(state_db) = thread.state_db() {
|
||||
return Ok(state_db);
|
||||
}
|
||||
} else {
|
||||
match find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string()).await
|
||||
{
|
||||
Ok(Some(_)) => {}
|
||||
Ok(None) => {
|
||||
return Err(invalid_request(format!("thread not found: {thread_id}")));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to locate thread id {thread_id}: {err}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open_state_db_for_direct_thread_lookup(&self.config)
|
||||
.await
|
||||
.ok_or_else(|| internal_error("sqlite state db unavailable for thread goals"))
|
||||
}
|
||||
|
||||
pub(super) async fn emit_thread_goal_snapshot(&self, thread_id: ThreadId) {
|
||||
let state_db = match self.state_db_for_materialized_thread(thread_id).await {
|
||||
Ok(state_db) => state_db,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to open state db before emitting thread goal resume snapshot for {thread_id}: {}",
|
||||
err.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let listener_command_tx = {
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
let thread_state = thread_state.lock().await;
|
||||
thread_state.listener_command_tx()
|
||||
};
|
||||
if let Some(listener_command_tx) = listener_command_tx {
|
||||
let command = crate::thread_state::ThreadListenerCommand::EmitThreadGoalSnapshot {
|
||||
state_db: state_db.clone(),
|
||||
};
|
||||
if listener_command_tx.send(command).is_ok() {
|
||||
return;
|
||||
}
|
||||
warn!(
|
||||
"failed to enqueue thread goal snapshot for {thread_id}: listener command channel is closed"
|
||||
);
|
||||
}
|
||||
send_thread_goal_snapshot_notification(&self.outgoing, thread_id, &state_db).await;
|
||||
}
|
||||
|
||||
async fn emit_thread_goal_updated_ordered(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
goal: ThreadGoal,
|
||||
listener_command_tx: Option<tokio::sync::mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
) {
|
||||
if let Some(listener_command_tx) = listener_command_tx {
|
||||
let command = crate::thread_state::ThreadListenerCommand::EmitThreadGoalUpdated {
|
||||
goal: goal.clone(),
|
||||
};
|
||||
if listener_command_tx.send(command).is_ok() {
|
||||
return;
|
||||
}
|
||||
warn!(
|
||||
"failed to enqueue thread goal update for {thread_id}: listener command channel is closed"
|
||||
);
|
||||
}
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalUpdated(
|
||||
ThreadGoalUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: None,
|
||||
goal,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn emit_thread_goal_cleared_ordered(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
listener_command_tx: Option<tokio::sync::mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
) {
|
||||
if let Some(listener_command_tx) = listener_command_tx {
|
||||
let command = crate::thread_state::ThreadListenerCommand::EmitThreadGoalCleared;
|
||||
if listener_command_tx.send(command).is_ok() {
|
||||
return;
|
||||
}
|
||||
warn!(
|
||||
"failed to enqueue thread goal clear for {thread_id}: listener command channel is closed"
|
||||
);
|
||||
}
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ThreadGoalCleared(
|
||||
ThreadGoalClearedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_goal_budget(value: Option<i64>) -> Result<(), String> {
|
||||
if let Some(value) = value
|
||||
&& value <= 0
|
||||
{
|
||||
return Err("goal budgets must be positive when provided".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thread_goal_status_to_state(status: ThreadGoalStatus) -> codex_state::ThreadGoalStatus {
|
||||
match status {
|
||||
ThreadGoalStatus::Active => codex_state::ThreadGoalStatus::Active,
|
||||
ThreadGoalStatus::Paused => codex_state::ThreadGoalStatus::Paused,
|
||||
ThreadGoalStatus::BudgetLimited => codex_state::ThreadGoalStatus::BudgetLimited,
|
||||
ThreadGoalStatus::Complete => codex_state::ThreadGoalStatus::Complete,
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_goal_status_from_state(status: codex_state::ThreadGoalStatus) -> ThreadGoalStatus {
|
||||
match status {
|
||||
codex_state::ThreadGoalStatus::Active => ThreadGoalStatus::Active,
|
||||
codex_state::ThreadGoalStatus::Paused => ThreadGoalStatus::Paused,
|
||||
codex_state::ThreadGoalStatus::BudgetLimited => ThreadGoalStatus::BudgetLimited,
|
||||
codex_state::ThreadGoalStatus::Complete => ThreadGoalStatus::Complete,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn api_thread_goal_from_state(goal: codex_state::ThreadGoal) -> ThreadGoal {
|
||||
ThreadGoal {
|
||||
thread_id: goal.thread_id.to_string(),
|
||||
objective: goal.objective,
|
||||
status: thread_goal_status_from_state(goal.status),
|
||||
token_budget: goal.token_budget,
|
||||
tokens_used: goal.tokens_used,
|
||||
time_used_seconds: goal.time_used_seconds,
|
||||
created_at: goal.created_at.timestamp(),
|
||||
updated_at: goal.updated_at.timestamp(),
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,9 @@ use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_PARAMS_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_params;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
@@ -158,7 +158,7 @@ impl CommandExecManager {
|
||||
} = params;
|
||||
if process_id.is_none() && (tty || stream_stdin || stream_stdout_stderr) {
|
||||
return Err(invalid_request(
|
||||
"command/exec tty or streaming requires a client-supplied processId".to_string(),
|
||||
"command/exec tty or streaming requires a client-supplied processId",
|
||||
));
|
||||
}
|
||||
let process_id = process_id.map_or_else(
|
||||
@@ -178,12 +178,12 @@ impl CommandExecManager {
|
||||
if matches!(exec_request.sandbox, SandboxType::WindowsRestrictedToken) {
|
||||
if tty || stream_stdin || stream_stdout_stderr {
|
||||
return Err(invalid_request(
|
||||
"streaming command/exec is not supported with windows sandbox".to_string(),
|
||||
"streaming command/exec is not supported with windows sandbox",
|
||||
));
|
||||
}
|
||||
if output_bytes_cap != Some(DEFAULT_OUTPUT_BYTES_CAP) {
|
||||
return Err(invalid_request(
|
||||
"custom outputBytesCap is not supported with windows sandbox".to_string(),
|
||||
"custom outputBytesCap is not supported with windows sandbox",
|
||||
));
|
||||
}
|
||||
if let InternalProcessId::Client(_) = &process_id {
|
||||
@@ -249,7 +249,7 @@ impl CommandExecManager {
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_request("command must not be empty".to_string()))?;
|
||||
.ok_or_else(|| invalid_request("command must not be empty"))?;
|
||||
{
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if sessions.contains_key(&process_key) {
|
||||
@@ -312,7 +312,7 @@ impl CommandExecManager {
|
||||
) -> Result<CommandExecWriteResponse, JSONRPCErrorError> {
|
||||
if params.delta_base64.is_none() && !params.close_stdin {
|
||||
return Err(invalid_params(
|
||||
"command/exec/write requires deltaBase64 or closeStdin".to_string(),
|
||||
"command/exec/write requires deltaBase64 or closeStdin",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ impl CommandExecManager {
|
||||
};
|
||||
let CommandExecSession::Active { control_tx } = session else {
|
||||
return Err(invalid_request(
|
||||
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes".to_string(),
|
||||
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes",
|
||||
));
|
||||
};
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
@@ -635,7 +635,7 @@ async fn handle_process_write(
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if !stream_stdin {
|
||||
return Err(invalid_request(
|
||||
"stdin streaming is not enabled for this command/exec".to_string(),
|
||||
"stdin streaming is not enabled for this command/exec",
|
||||
));
|
||||
}
|
||||
if !delta.is_empty() {
|
||||
@@ -643,7 +643,7 @@ async fn handle_process_write(
|
||||
.writer_sender()
|
||||
.send(delta)
|
||||
.await
|
||||
.map_err(|_| invalid_request("stdin is already closed".to_string()))?;
|
||||
.map_err(|_| invalid_request("stdin is already closed"))?;
|
||||
}
|
||||
if close_stdin {
|
||||
session.close_stdin();
|
||||
@@ -665,7 +665,7 @@ pub(crate) fn terminal_size_from_protocol(
|
||||
) -> Result<TerminalSize, JSONRPCErrorError> {
|
||||
if size.rows == 0 || size.cols == 0 {
|
||||
return Err(invalid_params(
|
||||
"command/exec size rows and cols must be greater than 0".to_string(),
|
||||
"command/exec size rows and cols must be greater than 0",
|
||||
));
|
||||
}
|
||||
Ok(TerminalSize {
|
||||
@@ -681,39 +681,13 @@ fn command_no_longer_running_error(process_id: &InternalProcessId) -> JSONRPCErr
|
||||
))
|
||||
}
|
||||
|
||||
fn invalid_request(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_params(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_PARAMS_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_error(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -730,13 +704,10 @@ mod tests {
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
|
||||
fn windows_sandbox_exec_request() -> ExecRequest {
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
};
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
ExecRequest::new(
|
||||
vec!["cmd".to_string()],
|
||||
AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
cwd,
|
||||
HashMap::new(),
|
||||
/*network*/ None,
|
||||
ExecExpiration::DefaultTimeout,
|
||||
@@ -744,9 +715,7 @@ mod tests {
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*windows_sandbox_private_desktop*/ false,
|
||||
sandbox_policy.clone(),
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
PermissionProfile::read_only(),
|
||||
/*arg0*/ None,
|
||||
)
|
||||
}
|
||||
@@ -836,10 +805,7 @@ mod tests {
|
||||
connection_id: ConnectionId(8),
|
||||
request_id: codex_app_server_protocol::RequestId::Integer(100),
|
||||
};
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
};
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
|
||||
manager
|
||||
.start(StartCommandExecParams {
|
||||
@@ -848,7 +814,7 @@ mod tests {
|
||||
process_id: Some("proc-100".to_string()),
|
||||
exec_request: ExecRequest::new(
|
||||
vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()],
|
||||
AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
cwd.clone(),
|
||||
HashMap::new(),
|
||||
/*network*/ None,
|
||||
ExecExpiration::Cancellation(CancellationToken::new()),
|
||||
@@ -856,9 +822,7 @@ mod tests {
|
||||
SandboxType::None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*windows_sandbox_private_desktop*/ false,
|
||||
sandbox_policy.clone(),
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
PermissionProfile::read_only(),
|
||||
/*arg0*/ None,
|
||||
),
|
||||
started_network_proxy: None,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::config_manager_service::ConfigManagerError;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use async_trait::async_trait;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
@@ -22,15 +23,15 @@ use codex_app_server_protocol::NetworkDomainPermission;
|
||||
use codex_app_server_protocol::NetworkRequirements;
|
||||
use codex_app_server_protocol::NetworkUnixSocketPermission;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_config::HookHandlerConfig as CoreHookHandlerConfig;
|
||||
use codex_config::ManagedHooksRequirementsToml;
|
||||
use codex_config::MatcherGroup as CoreMatcherGroup;
|
||||
use codex_config::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::ConfigRequirementsToml;
|
||||
use codex_core::config_loader::HookEventsToml;
|
||||
use codex_core::config_loader::HookHandlerConfig as CoreHookHandlerConfig;
|
||||
use codex_core::config_loader::ManagedHooksRequirementsToml;
|
||||
use codex_core::config_loader::MatcherGroup as CoreMatcherGroup;
|
||||
use codex_core::config_loader::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_core::plugins::PluginId;
|
||||
use codex_core_plugins::loader::installed_plugin_telemetry_metadata;
|
||||
use codex_core_plugins::toggles::collect_plugin_enabled_candidates;
|
||||
@@ -99,10 +100,10 @@ impl ConfigApi {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to resolve feature override precedence: {err}"),
|
||||
data: None,
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to resolve feature override precedence: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,14 +198,10 @@ impl ConfigApi {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"unsupported feature enablement `{key}`: currently supported features are {}",
|
||||
SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.join(", ")
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"unsupported feature enablement `{key}`: currently supported features are {}",
|
||||
SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
let message = if let Some(feature) = feature_for_key(key) {
|
||||
@@ -215,11 +212,7 @@ impl ConfigApi {
|
||||
} else {
|
||||
format!("invalid feature enablement `{key}`")
|
||||
};
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(message));
|
||||
}
|
||||
|
||||
if enablement.is_empty() {
|
||||
@@ -232,11 +225,7 @@ impl ConfigApi {
|
||||
.iter()
|
||||
.map(|(name, enabled)| (name.clone(), *enabled)),
|
||||
)
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "failed to update feature enablement".to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| internal_error("failed to update feature enablement"))?;
|
||||
|
||||
self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
self.user_config_reloader.reload_user_config().await;
|
||||
@@ -388,20 +377,20 @@ fn map_residency_requirement_to_api(
|
||||
}
|
||||
|
||||
fn map_network_requirements_to_api(
|
||||
network: codex_core::config_loader::NetworkRequirementsToml,
|
||||
network: codex_config::NetworkRequirementsToml,
|
||||
) -> NetworkRequirements {
|
||||
let allowed_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::allowed_domains);
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains);
|
||||
let denied_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::denied_domains);
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains);
|
||||
let allow_unix_sockets = network
|
||||
.unix_sockets
|
||||
.as_ref()
|
||||
.map(codex_core::config_loader::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.filter(|entries| !entries.is_empty());
|
||||
|
||||
NetworkRequirements {
|
||||
@@ -438,28 +427,20 @@ fn map_network_requirements_to_api(
|
||||
}
|
||||
|
||||
fn map_network_domain_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkDomainPermissionToml,
|
||||
permission: codex_config::NetworkDomainPermissionToml,
|
||||
) -> NetworkDomainPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Allow => {
|
||||
NetworkDomainPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Deny => {
|
||||
NetworkDomainPermission::Deny
|
||||
}
|
||||
codex_config::NetworkDomainPermissionToml::Allow => NetworkDomainPermission::Allow,
|
||||
codex_config::NetworkDomainPermissionToml::Deny => NetworkDomainPermission::Deny,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_network_unix_socket_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkUnixSocketPermissionToml,
|
||||
permission: codex_config::NetworkUnixSocketPermissionToml,
|
||||
) -> NetworkUnixSocketPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::Allow => {
|
||||
NetworkUnixSocketPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::None => {
|
||||
NetworkUnixSocketPermission::None
|
||||
}
|
||||
codex_config::NetworkUnixSocketPermissionToml::Allow => NetworkUnixSocketPermission::Allow,
|
||||
codex_config::NetworkUnixSocketPermissionToml::None => NetworkUnixSocketPermission::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,11 +449,7 @@ fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
||||
return config_write_error(code, err.to_string());
|
||||
}
|
||||
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
}
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
|
||||
fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) -> JSONRPCErrorError {
|
||||
@@ -491,13 +468,13 @@ mod tests {
|
||||
use crate::config_manager::apply_runtime_feature_enablement;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml as CoreNetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml as CoreNetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml as CoreNetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionsToml as CoreNetworkUnixSocketPermissionsToml;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NetworkDomainPermissionToml as CoreNetworkDomainPermissionToml;
|
||||
use codex_config::NetworkDomainPermissionsToml as CoreNetworkDomainPermissionsToml;
|
||||
use codex_config::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_config::NetworkUnixSocketPermissionToml as CoreNetworkUnixSocketPermissionToml;
|
||||
use codex_config::NetworkUnixSocketPermissionsToml as CoreNetworkUnixSocketPermissionsToml;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -539,11 +516,9 @@ mod tests {
|
||||
CoreSandboxModeRequirement::ExternalSandbox,
|
||||
]),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![
|
||||
codex_core::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: Some(codex_core::config_loader::FeatureRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: std::collections::BTreeMap::from([
|
||||
("apps".to_string(), false),
|
||||
("personality".to_string(), true),
|
||||
@@ -809,11 +784,9 @@ mod tests {
|
||||
)])
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(ConfigRequirementsToml {
|
||||
feature_requirements: Some(
|
||||
codex_core::config_loader::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("apps".to_string(), false)]),
|
||||
},
|
||||
),
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("apps".to_string(), false)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_requirements::cloud_requirements_loader;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::config_loader::load_config_layers_state;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::feature_for_key;
|
||||
use codex_login::AuthManager;
|
||||
@@ -33,7 +33,6 @@ pub(crate) struct ConfigManager {
|
||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
||||
host_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
@@ -44,27 +43,6 @@ impl ConfigManager {
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
) -> Self {
|
||||
Self::new_with_host_name(
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
arg0_paths,
|
||||
thread_config_loader,
|
||||
codex_config::host_name(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_with_host_name(
|
||||
codex_home: PathBuf,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
host_name: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -74,7 +52,6 @@ impl ConfigManager {
|
||||
cloud_requirements: Arc::new(RwLock::new(cloud_requirements)),
|
||||
arg0_paths,
|
||||
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
||||
host_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +206,6 @@ impl ConfigManager {
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_requirements(self.current_cloud_requirements())
|
||||
.thread_config_loader(self.current_thread_config_loader())
|
||||
.host_name(self.host_name.clone())
|
||||
.build()
|
||||
.await?;
|
||||
self.apply_runtime_feature_enablement(&mut config);
|
||||
@@ -257,7 +233,6 @@ impl ConfigManager {
|
||||
self.loader_overrides.clone(),
|
||||
self.current_cloud_requirements(),
|
||||
thread_config_loader.as_ref(),
|
||||
self.host_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -285,16 +260,14 @@ impl ConfigManager {
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
host_name: Option<String>,
|
||||
) -> Self {
|
||||
Self::new_with_host_name(
|
||||
Self::new(
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
Arg0DispatchPaths::default(),
|
||||
Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
host_name,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -305,7 +278,6 @@ impl ConfigManager {
|
||||
Vec::new(),
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::OverriddenMetadata;
|
||||
use codex_app_server_protocol::WriteStatus;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_core::config::deserialize_config_toml_with_base;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::validate_feature_requirements_for_config_toml;
|
||||
use codex_core::config_loader::ConfigLayerEntry;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::ConfigRequirementsToml;
|
||||
use codex_core::config_loader::merge_toml_values;
|
||||
use codex_core::path_utils;
|
||||
use codex_core::path_utils::SymlinkWritePaths;
|
||||
use codex_core::path_utils::resolve_symlink_write_paths;
|
||||
|
||||
@@ -4,9 +4,9 @@ use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::FeatureRequirementsToml;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -226,7 +226,6 @@ async fn read_includes_origins_and_layers() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -305,7 +304,6 @@ writable_roots = ["~/code"]
|
||||
vec![],
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -346,7 +344,6 @@ async fn write_value_reports_override() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let result = service
|
||||
@@ -446,7 +443,6 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -514,7 +510,6 @@ async fn write_value_rejects_feature_requirement_conflict() {
|
||||
..Default::default()
|
||||
}))
|
||||
}),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -561,7 +556,6 @@ async fn write_value_rejects_profile_feature_requirement_conflict() {
|
||||
..Default::default()
|
||||
}))
|
||||
}),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -612,7 +606,6 @@ async fn read_reports_managed_overrides_user_and_session_flags() {
|
||||
cli_overrides,
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -666,7 +659,6 @@ async fn write_value_reports_managed_override() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let result = service
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_app_server_protocol::DeviceKeyAlgorithm;
|
||||
@@ -13,6 +14,7 @@ use codex_app_server_protocol::DeviceKeySignPayload;
|
||||
use codex_app_server_protocol::DeviceKeySignResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_device_key::DeviceKeyBinding;
|
||||
use codex_device_key::DeviceKeyBindingStore;
|
||||
use codex_device_key::DeviceKeyCreateRequest;
|
||||
use codex_device_key::DeviceKeyError;
|
||||
use codex_device_key::DeviceKeyGetPublicRequest;
|
||||
@@ -24,14 +26,29 @@ use codex_device_key::RemoteControlClientConnectionAudience;
|
||||
use codex_device_key::RemoteControlClientConnectionSignPayload;
|
||||
use codex_device_key::RemoteControlClientEnrollmentAudience;
|
||||
use codex_device_key::RemoteControlClientEnrollmentSignPayload;
|
||||
use codex_state::DeviceKeyBindingRecord;
|
||||
use codex_state::StateRuntime;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DeviceKeyApi {
|
||||
store: DeviceKeyStore,
|
||||
}
|
||||
|
||||
impl DeviceKeyApi {
|
||||
pub(crate) fn create(
|
||||
pub(crate) fn new(sqlite_home: PathBuf, default_provider: String) -> Self {
|
||||
Self {
|
||||
store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(
|
||||
sqlite_home,
|
||||
default_provider,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
&self,
|
||||
params: DeviceKeyCreateParams,
|
||||
) -> Result<DeviceKeyCreateResponse, JSONRPCErrorError> {
|
||||
@@ -44,11 +61,12 @@ impl DeviceKeyApi {
|
||||
client_id: params.client_id,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(create_response_from_info(info))
|
||||
}
|
||||
|
||||
pub(crate) fn public(
|
||||
pub(crate) async fn public(
|
||||
&self,
|
||||
params: DeviceKeyPublicParams,
|
||||
) -> Result<DeviceKeyPublicResponse, JSONRPCErrorError> {
|
||||
@@ -57,11 +75,12 @@ impl DeviceKeyApi {
|
||||
.get_public(DeviceKeyGetPublicRequest {
|
||||
key_id: params.key_id,
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(public_response_from_info(info))
|
||||
}
|
||||
|
||||
pub(crate) fn sign(
|
||||
pub(crate) async fn sign(
|
||||
&self,
|
||||
params: DeviceKeySignParams,
|
||||
) -> Result<DeviceKeySignResponse, JSONRPCErrorError> {
|
||||
@@ -71,6 +90,7 @@ impl DeviceKeyApi {
|
||||
key_id: params.key_id,
|
||||
payload: payload_from_params(params.payload),
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(DeviceKeySignResponse {
|
||||
signature_der_base64: STANDARD.encode(signature.signature_der),
|
||||
@@ -80,6 +100,77 @@ impl DeviceKeyApi {
|
||||
}
|
||||
}
|
||||
|
||||
struct StateDeviceKeyBindingStore {
|
||||
sqlite_home: PathBuf,
|
||||
default_provider: String,
|
||||
state_db: OnceCell<Arc<StateRuntime>>,
|
||||
}
|
||||
|
||||
impl StateDeviceKeyBindingStore {
|
||||
fn new(sqlite_home: PathBuf, default_provider: String) -> Self {
|
||||
Self {
|
||||
sqlite_home,
|
||||
default_provider,
|
||||
state_db: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn state_db(&self) -> Result<Arc<StateRuntime>, DeviceKeyError> {
|
||||
let sqlite_home = self.sqlite_home.clone();
|
||||
let default_provider = self.default_provider.clone();
|
||||
self.state_db
|
||||
.get_or_try_init(|| async move {
|
||||
StateRuntime::init(sqlite_home, default_provider)
|
||||
.await
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StateDeviceKeyBindingStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StateDeviceKeyBindingStore")
|
||||
.field("sqlite_home", &self.sqlite_home)
|
||||
.field("default_provider", &self.default_provider)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceKeyBindingStore for StateDeviceKeyBindingStore {
|
||||
async fn get_binding(&self, key_id: &str) -> Result<Option<DeviceKeyBinding>, DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
state_db
|
||||
.get_device_key_binding(key_id)
|
||||
.await
|
||||
.map(|record| {
|
||||
record.map(|record| DeviceKeyBinding {
|
||||
account_user_id: record.account_user_id,
|
||||
client_id: record.client_id,
|
||||
})
|
||||
})
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
}
|
||||
|
||||
async fn put_binding(
|
||||
&self,
|
||||
key_id: &str,
|
||||
binding: &DeviceKeyBinding,
|
||||
) -> Result<(), DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
state_db
|
||||
.upsert_device_key_binding(&DeviceKeyBindingRecord {
|
||||
key_id: key_id.to_string(),
|
||||
account_user_id: binding.account_user_id.clone(),
|
||||
client_id: binding.client_id.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_response_from_info(info: DeviceKeyInfo) -> DeviceKeyCreateResponse {
|
||||
DeviceKeyCreateResponse {
|
||||
key_id: info.key_id,
|
||||
@@ -211,16 +302,13 @@ fn protection_class_from_store(
|
||||
}
|
||||
|
||||
fn map_device_key_error(error: DeviceKeyError) -> JSONRPCErrorError {
|
||||
let code = match error {
|
||||
match &error {
|
||||
DeviceKeyError::DegradedProtectionNotAllowed { .. }
|
||||
| DeviceKeyError::HardwareBackedKeysUnavailable
|
||||
| DeviceKeyError::KeyNotFound
|
||||
| DeviceKeyError::InvalidPayload(_) => INVALID_REQUEST_ERROR_CODE,
|
||||
DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => INTERNAL_ERROR_CODE,
|
||||
};
|
||||
JSONRPCErrorError {
|
||||
code,
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
| DeviceKeyError::InvalidPayload(_) => invalid_request(error.to_string()),
|
||||
DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => {
|
||||
internal_error(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user