Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ AGENTS_API_PROXY_TOKEN_FILE=~/.parsar/agents-api/web-token
# exact workspace_directory root.
# AGENTS_CORE_WEB_ENVIRONMENT_FILES=1

# Optional local-only Docker backend recovery guide shown in the connection
# panel. Web renders copyable `docker start` and loopback health commands; it
# never accesses the Docker socket or executes them. Values are compiled into
# the browser bundle and must contain non-secret container names only.
# AGENTS_CORE_WEB_DOCKER_BACKEND_GUIDE=1
# AGENTS_CORE_WEB_DOCKER_DATABASE_CONTAINER=parsar-agents-api-web-smoke-db
# AGENTS_CORE_WEB_DOCKER_API_CONTAINER=agents-core-web-api
# AGENTS_CORE_WEB_DOCKER_DAEMON_CONTAINER=agents-core-web-daemon
# AGENTS_CORE_WEB_DOCKER_CORE_PORT=8091

# Optional local-only Docker connection recipe. This renders a copyable command;
# it never gives the browser Docker access or reads the credential file. Every
# value below is compiled into the browser bundle, so values must be non-secret.
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ runs on caller-managed Linux executor compute. See
[Connecting Agent Core](docs/core-connection.md#optional-self-hosted-session-creation)
for the exact boundary.

The separately opt-in `AGENTS_CORE_WEB_DOCKER_BACKEND_GUIDE=1` profile turns Dashboard
gateway failures into an explicit recovery entry point. For an existing stack, the
connection panel shows validated, copyable commands for the configured database, Core
API, and daemon containers plus the loopback health check. For first-time use, it shows
the Core image build command and immutable Parsar container/daemon setup links. Parsar
still requires operator-created database and credential state; Web never runs these
commands, accesses Docker, or invents secrets. Container names remain non-secret
operator configuration from `.env.example`.

For the reviewed local loopback stack, an operator can additionally enable the
default-off `AGENTS_CORE_WEB_DOCKER_GUIDE=1` profile and its required non-secret
`AGENTS_CORE_WEB_DOCKER_*` settings from `.env.example`. The connection panel then
Expand Down
6 changes: 6 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ Core 返回的 Environment ID、executor origin、连接状态和安全 launcher
运维方签发的 executor credential 文件始终留在 Web 之外。完整边界见
[连接 Agent Core](docs/core-connection.md#optional-self-hosted-session-creation)。

可单独启用 `AGENTS_CORE_WEB_DOCKER_BACKEND_GUIDE=1`。Dashboard 遇到网关错误时会明确
提示 Core 后端未就绪。已有容器时,连接面板会根据 `.env.example` 中经过校验的非秘密
容器名,展示数据库、Core API、daemon 和 loopback 健康检查的可复制命令;首次使用时,
则展示 Core 镜像构建命令以及固定 Parsar 版本的容器和 daemon 初始化文档。Parsar 首次
初始化仍需要运维方创建独立数据库和凭据,Web 不执行命令、不访问 Docker,也不猜测密钥。

对于已核对的本地 loopback 栈,还可以配置 `.env.example` 中默认关闭的
`AGENTS_CORE_WEB_DOCKER_GUIDE=1` 以及完整的非秘密 `AGENTS_CORE_WEB_DOCKER_*`
参数。连接面板会在原生 launcher 之外提供可复制的 Docker 命令;Web 仍不会读取
Expand Down
133 changes: 94 additions & 39 deletions apps/web/e2e/agents-lifecycle.spec.ts

Large diffs are not rendered by default.

30 changes: 26 additions & 4 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
} from "./lib/connection";
import { settleCollection } from "./lib/collection-load";
import { listStableCollectionPages } from "./lib/collection-pagination";
import { BACKEND_NOT_READY_NOTICE, backendFailureStatus } from "./lib/core-readiness";
import {
beginPendingFunctionResult,
failPendingFunctionResult,
Expand Down Expand Up @@ -120,6 +121,14 @@ import {

type View = ProductView | "system";

function viewFromLocation(): View {
if (typeof window === "undefined") return "dashboard";
const candidate = window.location.hash.slice(1);
return candidate === "agents" || candidate === "sessions" || candidate === "vaults" || candidate === "system"
? candidate
: "dashboard";
}

interface StreamConnection {
sessionId: string | null;
state: StreamState;
Expand Down Expand Up @@ -214,9 +223,22 @@ function projectTextEvent(current: SessionItem[], event: SessionEvent): SessionI

export function App() {
const { show: showToast } = useToast();
const [view, setView] = useState<View>("sessions");
const [view, setView] = useState<View>(viewFromLocation);
const [connection, setConnection] = useState<CoreConnection>(() => loadConnection());
const [connectionOpen, setConnectionOpen] = useState(false);

useEffect(() => {
const hash = view === "dashboard" ? "" : `#${view}`;
const next = `${window.location.pathname}${window.location.search}${hash}`;
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (current !== next) window.history.replaceState(window.history.state, "", next);
}, [view]);

useEffect(() => {
const onHashChange = () => setView(viewFromLocation());
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
const [agents, setAgents] = useState<SavedAgent[]>([]);
const [vaultCatalog, setVaultCatalog] = useState<VaultCatalog | null>(null);
const [vaultCollectionState, setVaultCollectionState] = useState<CoreConnectionState>("connecting");
Expand Down Expand Up @@ -381,7 +403,7 @@ export function App() {
const message = errorMessage(result.reason);
setAgentCollectionState("failed");
setAgentCollectionError(message);
notify(message, "error");
notify(backendFailureStatus(result.reason) ? BACKEND_NOT_READY_NOTICE : message, "error");
return false;
}
if (result.value === null) {
Expand Down Expand Up @@ -425,7 +447,7 @@ export function App() {
const message = errorMessage(result.reason);
setSessionCollectionState("failed");
setSessionCollectionError(message);
notify(message, "error");
notify(backendFailureStatus(result.reason) ? BACKEND_NOT_READY_NOTICE : message, "error");
return false;
}
if (result.value === null) {
Expand Down Expand Up @@ -516,7 +538,7 @@ export function App() {
const message = errorMessage(result.reason);
setFilteredSessionCollectionState("failed");
setFilteredSessionCollectionError(message);
notify(message, "error");
notify(backendFailureStatus(result.reason) ? BACKEND_NOT_READY_NOTICE : message, "error");
return false;
}
if (result.value === null) {
Expand Down
222 changes: 222 additions & 0 deletions apps/web/src/components/ConnectionModal.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,213 @@
margin: 8px 0 0;
}

.connection-docker-guide {
margin-top: 10px;
padding: 12px;
color: var(--fg-muted);
font-size: 12px;
line-height: 17px;
background: var(--surface-subtle);
border: 1px solid var(--line);
border-radius: 6px;
}

.connection-docker-guide .connection-guide-header > svg {
color: var(--accent);
}

.connection-docker-path {
margin-top: 12px;
padding: 10px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
}

.connection-docker-path-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}

.connection-docker-path-heading strong {
color: var(--fg);
font-size: 12px;
font-weight: 550;
}

.connection-docker-path-heading span {
color: var(--fg-muted);
font-size: 10px;
}

.connection-docker-path-heading code {
color: var(--fg);
font-family: var(--font-mono);
font-size: 10px;
}

.connection-docker-steps {
display: grid;
margin: 8px 0 0;
padding: 0;
gap: 10px;
counter-reset: docker-step;
list-style: none;
}

.connection-docker-first-use > p {
margin: 8px 0 0;
}

.connection-docker-first-steps {
display: grid;
margin: 9px 0 0;
padding-left: 20px;
gap: 8px;
}

.connection-docker-first-steps > li {
padding-left: 2px;
}

.connection-docker-first-steps > li > span {
display: grid;
gap: 1px;
}

.connection-docker-first-steps strong {
color: var(--fg);
font-size: 11px;
font-weight: 550;
}

.connection-docker-first-steps small {
color: var(--fg-muted);
font-size: 10px;
line-height: 14px;
}

.connection-docker-first-steps .connection-docker-command {
margin-top: 5px;
}

.connection-docker-first-links {
display: flex;
margin-top: 10px;
align-items: flex-start;
flex-direction: column;
gap: 5px;
}

.connection-docker-first-links a {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--accent-strong);
text-decoration: none;
}

.connection-docker-first-links a:hover {
text-decoration: underline;
}

.connection-docker-steps > li {
display: grid;
min-width: 0;
padding-top: 10px;
gap: 6px;
border-top: 1px solid var(--line);
counter-increment: docker-step;
}

.connection-docker-steps > li > span {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 2px 7px;
}

.connection-docker-steps > li > span::before {
display: grid;
width: 18px;
height: 18px;
grid-row: 1 / span 2;
place-items: center;
color: var(--accent);
font-family: var(--font-mono);
font-size: 10px;
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
border-radius: 999px;
content: counter(docker-step);
}

.connection-docker-steps strong,
.connection-docker-unconfigured strong {
color: var(--fg);
font-size: 12px;
font-weight: 550;
}

.connection-docker-steps small {
color: var(--fg-muted);
font-size: 11px;
line-height: 15px;
}

.connection-docker-command {
display: flex;
min-width: 0;
align-items: center;
padding: 7px 7px 7px 9px;
gap: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 5px;
}

.connection-docker-command code {
min-width: 0;
flex: 1;
overflow-x: auto;
color: var(--fg);
font-family: var(--font-mono);
font-size: 10px;
line-height: 15px;
white-space: nowrap;
}

.connection-docker-command .button {
min-height: 26px;
flex: 0 0 auto;
padding: 3px 7px;
font-size: 10px;
}

.connection-docker-unconfigured {
margin-top: 10px;
padding: 9px 10px;
background: var(--surface);
border: 1px dashed var(--line-strong);
border-radius: 5px;
}

.connection-docker-unconfigured p,
.connection-docker-boundary {
margin: 3px 0 0;
}

.connection-docker-unconfigured code {
color: var(--fg);
font-family: var(--font-mono);
font-size: 10px;
}

.connection-docker-boundary {
padding-top: 9px;
border-top: 1px solid var(--line);
}

.connection-modal-advanced {
margin-top: 12px;
}
Expand Down Expand Up @@ -207,4 +414,19 @@
grid-template-columns: 1fr;
gap: 2px;
}

.connection-docker-command {
align-items: stretch;
flex-direction: column;
}

.connection-docker-command .button {
width: 100%;
}

.connection-docker-path-heading {
align-items: flex-start;
flex-direction: column;
gap: 2px;
}
}
Loading
Loading