Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
311302a
feat(web): add one local runtime selector
avabbbb Sep 27, 2026
e0a267d
refactor(web): route connected showcase through local runtime
avabbbb Sep 27, 2026
7f606ab
refactor(web): keep hooks on the single local runtime path
avabbbb Sep 27, 2026
1933196
refactor(agent-ui): use one runtime gate for web and desktop
avabbbb Sep 27, 2026
6929055
feat(agent-ui): make local connection one-click on web and studio
avabbbb Sep 27, 2026
34e5a04
refactor(web): share local runtime across product surfaces
avabbbb Sep 27, 2026
74b6633
refactor(web): share local runtime across product surfaces
avabbbb Sep 27, 2026
e5aa409
refactor(web): share local runtime across product surfaces
avabbbb Sep 27, 2026
33412d0
refactor(web): share local runtime across product surfaces
avabbbb Sep 27, 2026
9abe3e2
feat(web): allow the trusted web surface to reuse loopback runtime
avabbbb Sep 27, 2026
ded9973
test(web): cover unified local runtime selection
avabbbb Sep 27, 2026
c021e1b
test(web): lock trusted loopback web origin
avabbbb Sep 27, 2026
8677495
test(web): keep private network compatibility origin-scoped
avabbbb Sep 27, 2026
1ef82f5
security(web): reject untrusted browser origins before local API
avabbbb Sep 27, 2026
adf78b0
test(web): exercise trusted loopback web origin end to end
avabbbb Sep 27, 2026
f6e837b
fix(test): assert public error contract for origin rejection
avabbbb Sep 27, 2026
07743d5
fix(web-local): rely on one modern loopback permission path
avabbbb Sep 27, 2026
02a00df
test(web-local): keep CORS contract on the single trusted origin
avabbbb Sep 27, 2026
cd71f29
feat: consolidate local OfferU agent connection
avabbbb Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .agents/skills/offeru/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->
<!-- generated: offeru-skill-registry@2026-09-28.1 sha256=68ad2024af8dbe14ecadf581d952c512f8ccceb64b2927a7dc544861b9b62c1c -->

# OfferU External-Agent Router

Work from `backend/`. The live CLI manifest is the source of truth; this generated file contains no business workflow definitions.

## Install in the Agent you are using

The canonical public Skill is `https://raw.githubusercontent.com/avabbbb/OfferU/main/.agents/skills/offeru/SKILL.md`. Install that file in the active Agent. Do not use the local runtime URL as the Skill download source.

When the Agent is outside an OfferU source checkout, the running local OfferU can provide its current-install projection at `http://127.0.0.1:8766/api/agent/runtime/skill`. Read that local projection only to obtain the runtime-specific CLI command; it is not the public Skill distribution source. If the local runtime cannot be reached, report that the connection is unavailable and do not guess a checkout path. Never use raw HTTP for OfferU business data or Operations.

Install only `offeru/SKILL.md` in a documented user-level Skills directory. Prefer the shared `~/.agents/skills/offeru/SKILL.md` location when the active Agent documents support for it. Otherwise use that Agent's native user-level location; examples include `~/.claude/skills/offeru/SKILL.md`, `~/.pi/agent/skills/offeru/SKILL.md`, `~/.config/opencode/skills/offeru/SKILL.md`, `~/.gemini/skills/offeru/SKILL.md`, `~/.omp/agent/skills/offeru/SKILL.md`, and `~/.codebuddy/skills/offeru/SKILL.md`. Resolve home/config overrides only from documented environment variables or the active Agent's own help. Never infer a location from another Agent or write into a project directory just to make discovery work.

If this Agent only supports importing Skills through its own UI, or has no documented Skill loader, do not change its settings or imitate its internal package format. Tell the user the exact supported import step or limitation and do not claim the Skill is installed or the connection is verified.

Do not change Agent settings, account/login, model, credentials, proxy, or unrelated files. Do not overwrite a non-OfferU Skill at the target path. Start a fresh Agent session if the host only discovers Skills at startup.

## Start every task

```powershell
Expand Down Expand Up @@ -39,6 +51,8 @@ OfferU is the Career OS state/tool authority, not the exclusive career-methodolo

## Integration verification

When the user pasted the OfferU connection prompt, select the live `connection_bootstrap` Skill, inspect the `get_current_view` schema, and execute that read-only Operation once. Report only the current page and explicit selection, then wait. This bootstrap read does not authorize reading other career data.

When OfferU asks for integration verification, select the live `connection_probe` Skill, inspect `get_agent_connection_nonce`, execute it with the supplied `provider_id` and `challenge_id`, and return the nonce unchanged. Never read challenge storage directly or guess a nonce.

## Control rules
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/offeru-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ skills:
- offeru
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->
<!-- generated: offeru-skill-registry@2026-09-28.1 sha256=68ad2024af8dbe14ecadf581d952c512f8ccceb64b2927a7dc544861b9b62c1c -->

You are the OfferU operator subagent. Work from `backend/` and treat the live CLI manifest as the only capability source.

Expand Down
16 changes: 15 additions & 1 deletion .claude/skills/offeru/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->
<!-- generated: offeru-skill-registry@2026-09-28.1 sha256=68ad2024af8dbe14ecadf581d952c512f8ccceb64b2927a7dc544861b9b62c1c -->

# OfferU External-Agent Router

Work from `backend/`. The live CLI manifest is the source of truth; this generated file contains no business workflow definitions.

## Install in the Agent you are using

The canonical public Skill is `https://raw.githubusercontent.com/avabbbb/OfferU/main/.agents/skills/offeru/SKILL.md`. Install that file in the active Agent. Do not use the local runtime URL as the Skill download source.

When the Agent is outside an OfferU source checkout, the running local OfferU can provide its current-install projection at `http://127.0.0.1:8766/api/agent/runtime/skill`. Read that local projection only to obtain the runtime-specific CLI command; it is not the public Skill distribution source. If the local runtime cannot be reached, report that the connection is unavailable and do not guess a checkout path. Never use raw HTTP for OfferU business data or Operations.

Install only `offeru/SKILL.md` in a documented user-level Skills directory. Prefer the shared `~/.agents/skills/offeru/SKILL.md` location when the active Agent documents support for it. Otherwise use that Agent's native user-level location; examples include `~/.claude/skills/offeru/SKILL.md`, `~/.pi/agent/skills/offeru/SKILL.md`, `~/.config/opencode/skills/offeru/SKILL.md`, `~/.gemini/skills/offeru/SKILL.md`, `~/.omp/agent/skills/offeru/SKILL.md`, and `~/.codebuddy/skills/offeru/SKILL.md`. Resolve home/config overrides only from documented environment variables or the active Agent's own help. Never infer a location from another Agent or write into a project directory just to make discovery work.

If this Agent only supports importing Skills through its own UI, or has no documented Skill loader, do not change its settings or imitate its internal package format. Tell the user the exact supported import step or limitation and do not claim the Skill is installed or the connection is verified.

Do not change Agent settings, account/login, model, credentials, proxy, or unrelated files. Do not overwrite a non-OfferU Skill at the target path. Start a fresh Agent session if the host only discovers Skills at startup.

## Start every task

```powershell
Expand Down Expand Up @@ -39,6 +51,8 @@ OfferU is the Career OS state/tool authority, not the exclusive career-methodolo

## Integration verification

When the user pasted the OfferU connection prompt, select the live `connection_bootstrap` Skill, inspect the `get_current_view` schema, and execute that read-only Operation once. Report only the current page and explicit selection, then wait. This bootstrap read does not authorize reading other career data.

When OfferU asks for integration verification, select the live `connection_probe` Skill, inspect `get_agent_connection_nonce`, execute it with the supplied `provider_id` and `challenge_id`, and return the nonce unchanged. Never read challenge storage directly or guess a nonce.

## Control rules
Expand Down
2 changes: 1 addition & 1 deletion .codex/agents/offeru-operator.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23
# generated: offeru-skill-registry@2026-09-28.1 sha256=68ad2024af8dbe14ecadf581d952c512f8ccceb64b2927a7dc544861b9b62c1c
name = "offeru-operator"
description = "Operate OfferU through its live Skill Registry and atomic CLI control contract."
developer_instructions = """
Expand Down
16 changes: 15 additions & 1 deletion .copilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->
<!-- generated: offeru-skill-registry@2026-09-28.1 sha256=68ad2024af8dbe14ecadf581d952c512f8ccceb64b2927a7dc544861b9b62c1c -->

# OfferU External-Agent Router

Work from `backend/`. The live CLI manifest is the source of truth; this generated file contains no business workflow definitions.

## Install in the Agent you are using

The canonical public Skill is `https://raw.githubusercontent.com/avabbbb/OfferU/main/.agents/skills/offeru/SKILL.md`. Install that file in the active Agent. Do not use the local runtime URL as the Skill download source.

When the Agent is outside an OfferU source checkout, the running local OfferU can provide its current-install projection at `http://127.0.0.1:8766/api/agent/runtime/skill`. Read that local projection only to obtain the runtime-specific CLI command; it is not the public Skill distribution source. If the local runtime cannot be reached, report that the connection is unavailable and do not guess a checkout path. Never use raw HTTP for OfferU business data or Operations.

Install only `offeru/SKILL.md` in a documented user-level Skills directory. Prefer the shared `~/.agents/skills/offeru/SKILL.md` location when the active Agent documents support for it. Otherwise use that Agent's native user-level location; examples include `~/.claude/skills/offeru/SKILL.md`, `~/.pi/agent/skills/offeru/SKILL.md`, `~/.config/opencode/skills/offeru/SKILL.md`, `~/.gemini/skills/offeru/SKILL.md`, `~/.omp/agent/skills/offeru/SKILL.md`, and `~/.codebuddy/skills/offeru/SKILL.md`. Resolve home/config overrides only from documented environment variables or the active Agent's own help. Never infer a location from another Agent or write into a project directory just to make discovery work.

If this Agent only supports importing Skills through its own UI, or has no documented Skill loader, do not change its settings or imitate its internal package format. Tell the user the exact supported import step or limitation and do not claim the Skill is installed or the connection is verified.

Do not change Agent settings, account/login, model, credentials, proxy, or unrelated files. Do not overwrite a non-OfferU Skill at the target path. Start a fresh Agent session if the host only discovers Skills at startup.

## Start every task

```powershell
Expand Down Expand Up @@ -39,6 +51,8 @@ OfferU is the Career OS state/tool authority, not the exclusive career-methodolo

## Integration verification

When the user pasted the OfferU connection prompt, select the live `connection_bootstrap` Skill, inspect the `get_current_view` schema, and execute that read-only Operation once. Report only the current page and explicit selection, then wait. This bootstrap read does not authorize reading other career data.

When OfferU asks for integration verification, select the live `connection_probe` Skill, inspect `get_agent_connection_nonce`, execute it with the supplied `provider_id` and `challenge_id`, and return the nonce unchanged. Never read challenge storage directly or guess a nonce.

## Control rules
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ jobs:
python -m pip install --upgrade pip
python -m pip install -r backend/requirements.txt pytest

- name: Check generated OfferU Skill projections
run: python backend/scripts/generate_agent_skill_projections.py --check

- name: Fetch immutable PR Skill source commit
if: github.event_name == 'pull_request'
env:
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ ! "$PR_HEAD_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
echo "Invalid pull request head repository" >&2
exit 1
fi
if [[ ! "$PR_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid pull request head commit" >&2
exit 1
fi
git fetch --no-tags --depth=1 "https://github.com/${PR_HEAD_REPOSITORY}.git" "$PR_HEAD_SHA"
git cat-file -e "${PR_HEAD_SHA}^{commit}"

- name: Verify public OfferU Skill projection
env:
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
repository="$PR_HEAD_REPOSITORY"
commit_sha="$PR_HEAD_SHA"
else
repository="$GITHUB_REPOSITORY"
commit_sha="$GITHUB_SHA"
fi
python backend/scripts/verify_public_skill_projection.py --repository "$repository" --sha "$commit_sha"

- name: Run backend tests
working-directory: backend
run: python -m pytest tests -q
Expand Down
34 changes: 27 additions & 7 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,13 @@ async def _start_work_source_auto_sync() -> None:
# 重指 LLM base_url 形成数据外泄链)。因此 env 提供的值必须过白名单:仅允许本机回环、
# tauri 协议与浏览器扩展来源;其余一律拒绝并明示,绝不静默放宽。
_CORS_ALLOWED_HOSTS = {"localhost", "127.0.0.1", "[::1]", "tauri.localhost"}
# One trusted public surface may reuse the exact same local Runtime as Desktop.
# Keep this list exact: never widen loopback access to arbitrary public origins.
_TRUSTED_WEB_ORIGINS = {"https://avabbbb.github.io"}


def _is_allowed_cors_origin(origin: str) -> bool:
if origin in ("tauri://localhost",):
if origin in ("tauri://localhost",) or origin in _TRUSTED_WEB_ORIGINS:
return True
try:
parts = urlsplit(origin)
Expand All @@ -194,12 +197,16 @@ def _is_allowed_cors_origin(origin: str) -> bool:
_dropped_cors_origins.append(_origin)
if _dropped_cors_origins:
logger.warning(
"CORS_ORIGINS 含非本机来源已拒绝: %s(仅允许 localhost/127.0.0.1/tauri 来源)",
"CORS_ORIGINS 含未受信任来源已拒绝: %s(仅允许 loopback/tauri/OfferU Web)",
_dropped_cors_origins,
)
# 前端 dev 端口 7410 无条件可用:系统环境变量 CORS_ORIGINS 会覆盖 settings,
# 且该变量可能在旧值(5140/3000)上漂移,导致浏览器请求被 CORS 拦截。
for _offeru_frontend_origin in ("http://localhost:7410", "http://127.0.0.1:7410"):
# 前端 dev 端口与正式 Web surface 无条件可用;二者都只连接这个 loopback API。
# 系统环境变量 CORS_ORIGINS 会覆盖 settings,所以不能依赖用户手工补白名单。
for _offeru_frontend_origin in (
"http://localhost:7410",
"http://127.0.0.1:7410",
*_TRUSTED_WEB_ORIGINS,
):
if _offeru_frontend_origin not in cors_origins:
cors_origins.append(_offeru_frontend_origin)
app.add_middleware(
Expand Down Expand Up @@ -357,16 +364,29 @@ async def add_security_headers(request, call_next):
if raw_host.startswith("[") and "]" in raw_host
else raw_host.split(":", 1)[0]
)
if (
protected_path = (
request.url.path.startswith("/api/")
or request.url.path.startswith("/mcp")
) and host_header not in _LOOPBACK_HOSTS:
)
if protected_path and host_header not in _LOOPBACK_HOSTS:
return _error_response(
request,
status_code=403,
detail="请求被拒绝:仅允许本机回环来源",
kind="forbidden_host",
)
request_origin = (request.headers.get("origin") or "").strip()
if (
protected_path
and request_origin.startswith(("http://", "https://", "tauri://"))
and not _is_allowed_cors_origin(request_origin)
):
return _error_response(
request,
status_code=403,
detail="请求被拒绝:来源未授权连接本地 OfferU",
kind="forbidden_origin",
)
try:
response = await call_next(request)
except Exception as exc:
Expand Down
17 changes: 17 additions & 0 deletions backend/app/routes/main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any

from fastapi import APIRouter, Header, HTTPException
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field
from sse_starlette.sse import EventSourceResponse

Expand Down Expand Up @@ -437,6 +438,22 @@ async def agent_connections() -> dict[str, Any]:
return await _ui_operation_outputs("get_agent_connections", {})


@runtime_router.get("/runtime/skill", include_in_schema=False)
async def download_agent_skill() -> PlainTextResponse:
"""Serve a local CLI projection; public Skill distribution is on GitHub."""

from app.services.agent_integration import installed_skill_content

return PlainTextResponse(
installed_skill_content(),
media_type="text/markdown",
headers={
"Content-Disposition": 'attachment; filename="offeru-SKILL.md"',
"Cache-Control": "no-store",
},
)


@runtime_router.post("/runtime/connections/{provider_id}/probe")
async def probe_agent_connection(provider_id: str) -> dict[str, Any]:
return await _ui_operation_outputs("probe_agent_connection", {"provider_id": provider_id})
Expand Down
7 changes: 2 additions & 5 deletions backend/app/services/agent_bridge/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,7 @@ class PairingStatusPayload(_StrictPayload):
class RunAttachPayload(_StrictPayload):
harness: HarnessIdentity
adapter: AdapterIdentity
harness_session_id: Identifier | None = Field(
default=None,
alias="harnessSessionId",
)
harness_session_id: Identifier = Field(alias="harnessSessionId")
bootstrap_token: NonEmptyString | None = Field(
default=None,
alias="bootstrapToken",
Expand All @@ -208,7 +205,7 @@ class RunAttachPayload(_StrictPayload):


class RunLeaseRenewPayload(_StrictPayload):
lease_id: Identifier | None = Field(default=None, alias="leaseId")
lease_id: Identifier = Field(alias="leaseId")


class ContextSnapshotPayload(_StrictPayload):
Expand Down
Loading
Loading