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
43 changes: 0 additions & 43 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,3 @@ OAC_WEB_DEV_PROXY_TARGET=http://127.0.0.1:8091

# Core reads its own environment, not this file. Core settings, including
# Runtime history sampling and export, are in docs/configuration.md.

# Public, non-secret opt-in for the reviewed Codex self_hosted Session profile.
# Leave unset unless Core execution, its executor registry, and executor origin
# are configured. This flag is presentation policy, not capability discovery.
# OAC_WEB_SELF_HOSTED_SESSIONS=1

# Public, non-secret opt-in for the operator-qualified basic Codex
# openai_hosted Session profile. Leave unset unless Core was started with a
# qualified managed Runtime provider. This flag does not probe runtime readiness.
# OAC_WEB_OPENAI_HOSTED_SESSIONS=1

# Public, non-secret opt-in for the complete Environment Files profile. Leave
# unset for older Core revisions. Enable only after the connected Core has been
# qualified for Files.list and managed Files.create; self_hosted reads use its
# exact workspace_directory root.
# OAC_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.
# OAC_WEB_DOCKER_BACKEND_GUIDE=1
# OAC_WEB_DOCKER_DATABASE_CONTAINER=oac-web-smoke-db
# OAC_WEB_DOCKER_API_CONTAINER=oac-web-smoke-api
# OAC_WEB_DOCKER_DAEMON_CONTAINER=oac-web-smoke-daemon
# OAC_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.
# Enable only for the matching operator-controlled local stack.
# OAC_WEB_DOCKER_GUIDE=1
# OAC_WEB_DOCKER_IMAGE=oac-web-smoke-executor:2b34ea46-codex-0.153.4
# OAC_WEB_DOCKER_API_CONTAINER=oac-web-smoke-api
# OAC_WEB_DOCKER_USER=501:20
# OAC_WEB_DOCKER_CREDENTIALS_HOME_PATH=.oac/web-smoke/executor-key.json
# OAC_WEB_DOCKER_RUNTIME_HOME_PATH=.oac/web-smoke/executors

# Public, non-secret suggestions shown by the Create Agent model picker. The
# first entry is the default unless VITE_AGENT_DEFAULT_MODEL overrides it. These
# do not claim live availability; the connected runtime remains authoritative.
VITE_AGENT_MODEL_PRESETS=gpt-6-astra,gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna,gpt-5.5,gpt-5.3-codex-spark
VITE_AGENT_DEFAULT_MODEL=gpt-5.6-sol
2 changes: 1 addition & 1 deletion apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ Every resource list, the Session log and the project list share one grammar:
- **ListToolbar**: on project-scoped lists the project filter first, then the SearchField (280px, search icon, Paper with the control ring), then any further filters (segmented status or order, selects); the count sits on the right in 12.5px Pencil ("12 total", "3 of 12", "40 loaded" when more exist).
- **Project column**: shown only while All projects is selected, right after the name; archived projects are muted.
- **NameCell**: the first column. The name at 500 weight (a link that turns indigo on hover when the row opens a detail page; a muted fallback such as "Untitled" when the resource has no name) with the compact ID underneath in 11.5px mono. The ID's copy button appears on row hover or focus; the full ID lives in its tooltip.
- **Creator column**: the last column before the actions, headed "Creator" with a help tip. It shows the creating key's name (its prefix when unnamed) with a small "Revoked" flag for revoked keys, "Admin copy" in Graphite for an asset Core records as an administrator copy, "Unknown" in Graphite when Core has no record, and "—" while loading or when the lookup failed.
- **Creator column**: the last column before the actions, headed "Creator" with a help tip. It shows the creating key's name (its prefix when unnamed) with a small "Revoked" flag for revoked keys, "Unknown" in Graphite when Core has no record, and "—" while loading or when the lookup failed.
- **RowActions**: text actions right-aligned at the end of the row, 16px apart, ending with Delete (red on hover). A row click opens the detail page; action clicks do not.
- **Partial failure**: when some projects fail to load, one red line names them above the table; the other projects still show.
- **Empty state**: an unframed, centered block with an optional 24px outline icon, a clear title, a visible short explanation and a relevant action. First-use states explain how data arrives; filtered states offer Clear search; failed reads retain their error and retry. Empty Overview activity links to Projects and keys for API onboarding.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The console runs beside the administrator's own Core, with execution, files and
- **Web API only.** Every read and write goes through `/core/v1/**`. The console holds no API key and sends nothing to `/v1`.
- **No asset writes except delete.** Assets are created and changed only by a project's keys through the Agents API. The console does not create or edit Agents or Templates, upload Skills or Files, create or replace Credentials, start Sessions, send input or cancel work. Deletion follows the public deletion rules; a busy Session is not deletable and the console never cancels work to make it so.
- **Secrets stay write-only.** Credential tokens, Template environment variables and setup commands are never returned, to the administrator included. An Agent's saved model provider shows its protocol, base URL, limits and whether a key is configured, never the key.
- **Creators.** Core records the key behind every write. The console shows the creating key of each asset and a project's write history; an asset Core records as an administrator copy (`admin_copy`) shows as Admin copy and an asset without a record as Unknown.
- **Creators.** Core records the key behind every write. The console shows the creating key of each asset and a project's write history; an asset without a record shows as Unknown.
- **Waiting for results.** Overview, Session log and Session details name the function whose result the calling application must submit. The console cannot submit that result; environment connection waits stay distinct from function waits.
- **Session history is read-only.** A Session page reads the Session, its Items and Turns and polls while work is in flight; there is no live event stream.
- **Failure diagnostics.** Failed Session and Turn rows read Core diagnostics and translate its classified reason. The console never infers a cause from raw logs. Unavailable or mismatched diagnostics offer an explicit read retry; refreshing does not replay execution. Trace Timing keeps each Item's Core receipt interval separate from public Turn times and native tool duration. Historical missing timestamps stay unknown, negative clock intervals stay missing, and bounded response truncation remains visible.
Expand Down
11 changes: 3 additions & 8 deletions apps/web/e2e/data/admin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function buildAdmin(now, base, resources) {
const cacheKey = `${type}:${id}`;
if (!creators.has(cacheKey)) {
turn += 1;
creators.set(cacheKey, turn % 11 === 0 ? { copy: true } : turn % 7 === 0 ? null : keyRef(project, project.keys[turn % project.keys.length]));
creators.set(cacheKey, turn % 7 === 0 ? null : keyRef(project, project.keys[turn % project.keys.length]));
}
return creators.get(cacheKey);
};
Expand All @@ -62,7 +62,6 @@ export function buildAdmin(now, base, resources) {
const list = [];
const push = (at, action, type, id, parent = "", creatorType = type) => {
const creator = action === "create" ? creatorFor(project, creatorType, id) : keyRef(project, project.keys[list.length % project.keys.length]);
if (creator?.copy) return; // Administrator copies are in the audit log, not in key write history.
list.push({ id: `op_${project.id.slice(5)}_${list.length}`, created_at: iso(at), api_key: creator, action, resource_type: type, resource_id: id, parent_id: parent, request_id: `req_${list.length}`, trace_id: `${list.length}`.padStart(32, "0") });
};
for (const agent of own.agents) { push(agent.created_at, "create", "agent", agent.id); if (agent.updated_at > agent.created_at) push(agent.updated_at, "update", "agent", agent.id); }
Expand All @@ -81,11 +80,7 @@ export function buildAdmin(now, base, resources) {
function resourceOwners(project, url) {
const type = url.searchParams.get("resource_type");
const ids = (url.searchParams.get("resource_ids") ?? "").split(",").filter(Boolean).slice(0, 100);
return { data: ids.map((id) => {
const creator = creatorFor(project, type, id);
if (creator?.copy) return { resource_id: id, api_key: null, source: "admin_copy", admin_audit_id: `audit_${id.slice(-8)}` };
return creator ? { resource_id: id, api_key: creator, source: "api_key", admin_audit_id: null } : { resource_id: id, api_key: null, source: null, admin_audit_id: null };
}) };
return { data: ids.map((id) => ({ resource_id: id, api_key: creatorFor(project, type, id) })) };
}

function summarize(sessions) {
Expand Down Expand Up @@ -143,7 +138,7 @@ export function buildAdmin(now, base, resources) {
const auditLog = () => {
const entries = [];
let n = 0;
const add = (at, action, project, type, id, results = []) => entries.push({ id: `audit_${String(++n).padStart(4, "0")}`, created_at: iso(at), admin_credential_id: "a1b2c3d4", actor_label: "admin", action, project_id: project.id, resource_type: type, resource_id: id, result_ids: results, request_id: `req_admin_${n}`, trace_id: `${n}`.padStart(32, "a") });
const add = (at, action, project, type, id) => entries.push({ id: `audit_${String(++n).padStart(4, "0")}`, created_at: iso(at), admin_credential_id: "a1b2c3d4", actor_label: "admin", action, project_id: project.id, resource_type: type, resource_id: id, request_id: `req_admin_${n}`, trace_id: `${n}`.padStart(32, "a") });
for (const project of projects) {
add(project.created_at, "create_project", project, "project", project.id);
for (const k of project.keys) { add(k.created_at, "issue_key", project, "api_key", k.id); if (k.revoked_at) add(k.revoked_at, "revoke_key", project, "api_key", k.id); }
Expand Down
50 changes: 2 additions & 48 deletions apps/web/e2e/data/resources.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,9 @@ let seed = 7;
const rand = () => ((seed = (seed * 1664525 + 1013904223) % 4294967296) / 4294967296);
const hex = (n) => Array.from({ length: n }, () => Math.floor(rand() * 16).toString(16)).join("");
const uuid = () => `${hex(8)}-${hex(4)}-4${hex(3)}-8${hex(3)}-${hex(12)}`;
const iso = (seconds) => new Date(seconds * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");

export function buildResources(now, agents, sessions) {
export function buildResources(now) {
seed = 7;
const keys = [
{ id: "fb533e99-524f-4e44-94bc-8f6e571646a7", name: "Production app", prefix: "pc_live_7Hq", created_at: iso(now - 86400 * 21), revoked_at: null },
{ id: "3c1d9e20-7a41-4b8e-9f02-5d6e7f8a9b10", name: "CI pipeline", prefix: "pc_live_Qm4", created_at: iso(now - 86400 * 16), revoked_at: null },
{ id: "a47e2b19-0c3d-4e5f-8a6b-7c8d9e0f1a2b", name: "Data team notebook", prefix: "pc_live_k9T", created_at: iso(now - 86400 * 9), revoked_at: null },
{ id: "0b533e99-524f-4e44-94bc-8f6e571646a7", name: "Staging", prefix: "pc_live_2Xa", created_at: iso(now - 86400 * 30), revoked_at: iso(now - 86400 * 6) },
];
const refOf = (key) => ({ type: "project_api_key", id: key.id, name: key.name, prefix: key.prefix, revoked_at: key.revoked_at });
const consoleRef = { type: "console", id: null, name: null, prefix: null, revoked_at: null };
// Weighted owner choice: most traffic from production, some unknown (created before recording).
const owners = [refOf(keys[0]), refOf(keys[0]), refOf(keys[0]), refOf(keys[1]), refOf(keys[1]), refOf(keys[2]), refOf(keys[3]), consoleRef, null];
const ownerOf = () => owners[Math.floor(rand() * owners.length)];

const skills = [
["report", "Create quarterly and incident reports from structured notes.", 3, 2],
["triage", "Sort incoming issues by severity and owner.", 2, 2],
Expand Down Expand Up @@ -72,41 +59,8 @@ export function buildResources(now, agents, sessions) {
created_at: created, updated_at: created + (index % 2 ? 86400 : 0) };
}).reverse()]));

const ownership = new Map();
const own = (type, id, created) => { const owner = ownerOf(); ownership.set(`${type}:${id}`, { resource_type: type, resource_id: id, owner, created_at: owner ? iso(created) : null }); return owner; };
const activity = [];
const record = (owner, action, type, id, at, parent = null) => {
if (!owner) return;
activity.push({ id: `act_${uuid()}`, object: "api_key.activity", created_at: iso(at), actor: owner, action, resource_type: type, resource_id: id, parent_resource_id: parent, trace_id: hex(32) });
};
for (const agent of agents) { const owner = own("agent", agent.id, agent.created_at); record(owner, "create", "agent", agent.id, agent.created_at); if (agent.updated_at > agent.created_at) record(owner, "update", "agent", agent.id, agent.updated_at); }
for (const session of sessions) { const owner = own("session", session.id, session.created_at); record(owner, "create", "session", session.id, session.created_at); if (session.last_active_at > session.created_at + 60) record(owner, "send", "session", session.id, session.last_active_at); }
for (const skill of skills) {
const owner = own("skill", skill.id, skill.created_at);
record(owner, "create", "skill", skill.id, skill.created_at);
for (const version of skillVersions.get(skill.id).slice(0, -1)) record(owner, "create", "skill_version", version.id, version.created_at, skill.id);
if (skill.default_version !== skill.latest_version) record(owner, "update", "skill", skill.id, skill.created_at + 86400);
}
for (const file of files) record(own("file", file.id, file.created_at), "create", "file", file.id, file.created_at);
for (const template of templates) { const owner = own("environment_template", template.id, template.created_at); record(owner, "create", "environment_template", template.id, template.created_at); record(owner, "update", "environment_template", template.id, template.updated_at); }
for (const { vault } of vaults) {
const owner = own("vault", vault.id, vault.created_at);
record(owner, "create", "vault", vault.id, vault.created_at);
for (const credential of credentials.get(vault.id)) {
own("vault_credential", credential.id, credential.created_at);
record(owner, "create", "vault_credential", credential.id, credential.created_at, vault.id);
if (credential.updated_at > credential.created_at) record(owner, "update", "vault_credential", credential.id, credential.updated_at, vault.id);
}
}
// Deleted resources still appear in the log.
record(refOf(keys[3]), "delete", "agent", `agent_${hex(8)}`, now - 86400 * 7);
record(refOf(keys[1]), "delete", "file", `file-${uuid()}`, now - 86400 * 2 - 600);
record(refOf(keys[1]), "delete", "skill_version", `skillver_${uuid()}`, now - 86400 * 3, skills[1].id);
activity.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));

return {
keys, skills, skillVersions, files, templates,
skills, skillVersions, files, templates,
vaults: vaults.map(({ vault }) => vault), credentials,
ownership, activity,
};
}
2 changes: 1 addition & 1 deletion apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "d
const screenshots = process.env.OAC_WEB_SCREENSHOT_DEMO === "1";
const now = Math.floor(Date.now() / 1000);
const base = (screenshots ? buildScreenshotDemo : buildDemo)(now, address === "local" ? LOCAL_URL : PUBLIC_URL);
const resources = buildResources(now, base.agents, base.sessions);
const resources = buildResources(now);
const admin = buildAdmin(now, base, resources);
// A fresh install: no project, Session or Runtime yet; Getting started leads.
if (fresh) for (const list of [admin.projects, base.sessions, base.observations, base.allocations]) list.splice(0);
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/features/dashboard/runtime-history.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AgentCoreError, type AgentSession, type CoreProjectReader, type RuntimeHistory } from "@oac/agents-client";
import { AgentCoreError, type AgentSession, type RuntimeHistory } from "@oac/agents-client";

import type { ProjectClient } from "../../lib/projects";
import type { RuntimeDashboardSnapshot } from "./runtime-snapshot";
import { deriveTokenThroughput, type RuntimeTrendSample, type RuntimeTrendTarget } from "./runtime-trends";

Expand Down Expand Up @@ -160,7 +161,7 @@ export function runtimeDurableTrendSamples(
return deriveTokenThroughput(samples);
}

export type RuntimeHistoryReader = Pick<CoreProjectReader, "retrieveRuntimeHistory">;
export type RuntimeHistoryReader = Pick<ProjectClient, "retrieveRuntimeHistory">;

function isNotFound(error: unknown): boolean {
return error instanceof AgentCoreError && error.status === 404;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/files/file-operations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
AgentCoreError,
type CoreProjectReader,
type PageOrder,
type SourceFileListEntry,
} from "@oac/agents-client";

import type { ProjectClient } from "../../lib/projects";
import { appendCollectionPage } from "../../lib/collection-pagination";

/** Core's single-upload bound for user_data Files. */
Expand Down Expand Up @@ -74,7 +74,7 @@ export function filterFiles(files: readonly SourceFileListEntry[], query: string

/** Reads one page after the loaded rows and applies the shared identity and cursor checks. */
export async function readFilesPage(
core: Pick<CoreProjectReader, "listSourceFiles">,
core: Pick<ProjectClient, "listSourceFiles">,
loaded: readonly SourceFileListEntry[],
order: PageOrder,
after: string | undefined,
Expand Down
7 changes: 0 additions & 7 deletions apps/web/src/features/fleet/fleet-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { queryOptions, type QueryClient } from "@tanstack/react-query";
import { SandboxAdminClient, type SandboxAllocation, type SandboxDeployment, type SandboxNode, type SandboxNodeHistoryRange } from "@oac/agents-client";

import { sandboxDeploymentQuery } from "../sandbox/sandbox-queries";
import { sandboxConsoleConfig } from "../sandbox/console-config";

export interface FleetSnapshot {
deployment: SandboxDeployment;
Expand All @@ -18,12 +17,6 @@ function client(): SandboxAdminClient {
return sandboxClient;
}

/** The console's own configuration: whether it holds a sandbox administration credential. */
export const consoleConfigQuery = queryOptions({
queryKey: ["console-config"],
queryFn: ({ signal }) => sandboxConsoleConfig(signal),
});

async function loadFleet(readAllocations: boolean, signal: AbortSignal, cache: QueryClient): Promise<FleetSnapshot> {
const sandbox = client();
const [deployment, nodes] = await Promise.all([
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/features/fleet/use-sandbox-fleet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { describe, expect, it } from "vitest";

import { gettingStartedSteps } from "../overview/getting-started";
import { node } from "../overview/test-fixtures";
import type { SandboxConsoleConfig } from "../sandbox/console-config";
import { sandboxDeploymentQuery } from "../sandbox/sandbox-queries";
import { consoleConfigQuery, fleetQuery, type FleetSnapshot } from "./fleet-queries";
import { fleetQuery, type FleetSnapshot } from "./fleet-queries";
import { FleetReadNotice } from "./FleetReadNotice";
import { fleetSnapshot, useSandboxFleet } from "./use-sandbox-fleet";

Expand All @@ -23,8 +22,6 @@ function Probe() {
function render(latest: SandboxDeployment, previous = configured, failed = false) {
const cache = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const snapshot: FleetSnapshot = { deployment: previous, nodes: [node("n1")], allocations: [], loadedAt: 1 };
const config: SandboxConsoleConfig = { sandbox_admin: true, node_installer: false, node_installer_sha256: "" };
cache.setQueryData<SandboxConsoleConfig | null>(consoleConfigQuery.queryKey, () => config);
cache.setQueryData(fleetQuery(false).queryKey, snapshot);
cache.setQueryData(sandboxDeploymentQuery.queryKey, latest);
if (failed) cache.getQueryCache().find({ queryKey: fleetQuery(false).queryKey })?.setState({ status: "error", error: new Error("inventory read failed") });
Expand Down
Loading
Loading