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: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ tools. This requirement does not add execution prerequisites to Files reads.

Skills and their immutable versions are Core-owned tenant resources, independent of
Sessions and native Skill installations. Serialize version allocation and pointer
mutations under the owning Skill row; preserve unique version identities across
mutations under the owning Skill row. Keep top-level name and description aligned
with the default version in the same transaction, including default-changing uploads;
nondefault uploads preserve that metadata. Preserve unique version identities across
concurrent uploads and deletion. Metadata reads never load or decrypt bundle bytes.
Encrypt bundle contents with a tenant, Skill and version binding using the existing
service cipher. Deleting a Skill reclaims its versions without affecting already
Expand Down Expand Up @@ -784,7 +786,11 @@ metadata insertion and bytes commit atomically; deletion removes metadata and
unlinks the object in one transaction. Keep OIDs private and authorize every
metadata/content/delete lookup by tenant before opening a body. Stream bounded
chunks; never hold an entire general Files upload in memory or use filenames as
filesystem paths. A read-only repeatable-read transaction preserves an admitted
filesystem paths. Public source download admission is separate from internal byte
consumption: reject direct downloads of the supported `user_data` purpose after
tenant-scoped metadata lookup; initialization and workspace copies keep their
authorized Store read. Core Web must not offer that unavailable download action.
A read-only repeatable-read transaction preserves an admitted
source snapshot across concurrent deletion. Resolve that snapshot before entering
the existing Environment write path; deleting a source does not undo a completed
workspace copy. Bound request/transaction lifetimes, roll back incomplete bodies,
Expand Down
13 changes: 4 additions & 9 deletions apps/web/e2e/fixture-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -843,15 +843,10 @@ const server = http.createServer(async (request, response) => {
if (request.headers["openai-beta"] != null) return sendError(response, 400, "Source Files do not accept the Agents beta header in this fixture.");
const source = state.sourceFiles.get(id);
if (!source) return sendError(response, 404, "Fixture Source File not found.");
response.writeHead(200, {
"content-type": "application/octet-stream",
"content-disposition": `attachment; filename="${source.metadata.filename}"`,
"content-length": source.data.length,
"cache-control": "no-store",
"x-content-type-options": "nosniff",
});
response.end(source.data);
return;
return sendJson(response, { error: {
message: "Not allowed to download files of purpose: user_data",
type: "invalid_request_error", code: null, param: null,
} }, 400);
}

const sourceFileMatch = url.pathname.match(/^\/v1\/files\/([^/]+)$/);
Expand Down
1 change: 1 addition & 0 deletions apps/web/e2e/vault-credentials.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ test("creates, replaces, uses, and deletes a write-only Vault Credential", async
await expect(vaultCard).toBeVisible();
await vaultCard.getByRole("button", { name: "Credential", exact: true }).click();
const credentialDialog = page.getByRole("dialog", { name: "Add static bearer Credential" });
await expect(credentialDialog.getByLabel("Name")).toBeFocused();
await credentialDialog.getByLabel("Name").fill("Private docs MCP");
await credentialDialog.getByLabel("Exact MCP server URL").fill(mcpURL);
await fillWriteOnlyBearer(credentialDialog);
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ export function App() {
const sourceFilesOperations = useMemo<SourceFilesOperations>(() => ({
uploadSourceFile: (input, options) => core.uploadSourceFile(input, options),
retrieveSourceFile: (fileId, options) => core.retrieveSourceFile(fileId, options),
downloadSourceFile: (fileId, options) => core.downloadSourceFile(fileId, options),
deleteSourceFile: (fileId, options) => core.deleteSourceFile(fileId, options),
retrieveEnvironment: (environmentId, options) => core.retrieveEnvironment(environmentId, options),
createEnvironmentFile: (environmentId, input, options) => core.createEnvironmentFile(environmentId, input, options),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/features/system/SourceFilesPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ function operations(): SourceFilesOperations {
return {
uploadSourceFile: vi.fn(),
retrieveSourceFile: vi.fn(),
downloadSourceFile: vi.fn(),
deleteSourceFile: vi.fn(),
retrieveEnvironment: vi.fn(),
createEnvironmentFile: vi.fn(),
Expand Down Expand Up @@ -38,6 +37,8 @@ describe("SourceFilesPanel", () => {
expect(html).toContain("512 MiB source · 50 MiB destination");
expect(html).toContain("Upload Source File");
expect(html).toContain("Operate by Core ID");
expect(html).toContain("Uploaded Source Files cannot be downloaded directly");
expect(html).not.toContain(">Download</button>");
expect(html).toContain("Core has no Source Files list API");
expect(html).toContain("Copy to hosted Workspace");
expect(html).toContain("Check Environment");
Expand Down
46 changes: 5 additions & 41 deletions apps/web/src/features/system/SourceFilesPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Download, FileUp, FolderInput, RefreshCw, Search, Trash2 } from "lucide-react";
import { FileUp, FolderInput, RefreshCw, Search, Trash2 } from "lucide-react";
import { useEffect, useRef, useState, type ChangeEvent } from "react";

import {
Expand All @@ -16,14 +16,13 @@ import "./SourceFilesPanel.css";
export interface SourceFilesOperations {
uploadSourceFile: AgentCore["uploadSourceFile"];
retrieveSourceFile: AgentCore["retrieveSourceFile"];
downloadSourceFile: AgentCore["downloadSourceFile"];
deleteSourceFile: AgentCore["deleteSourceFile"];
retrieveEnvironment: AgentCore["retrieveEnvironment"];
createEnvironmentFile: AgentCore["createEnvironmentFile"];
listEnvironmentFiles: AgentCore["listEnvironmentFiles"];
}

type Operation = "upload" | "retrieve" | "download" | "delete" | "environment" | "copy" | null;
type Operation = "upload" | "retrieve" | "delete" | "environment" | "copy" | null;
type NoticeTone = "info" | "success" | "error" | "warning";

interface Notice {
Expand Down Expand Up @@ -76,11 +75,6 @@ function parentDirectory(path: string): string {
return path.slice(0, path.lastIndexOf("/")) || "/workspace";
}

function safeDownloadFilename(metadata: SourceFile | null, fileId: string): string {
if (metadata?.id !== fileId) return "source-file.bin";
return metadata.filename.replace(/[\\/\0\r\n]/g, "_") || "source-file.bin";
}

function knownRejected(error: unknown): boolean {
return error instanceof AgentCoreError && error.status >= 400 && error.status < 500;
}
Expand Down Expand Up @@ -197,36 +191,6 @@ export function SourceFilesPanel({
}
};

const download = async () => {
if (!currentId) return;
const { controller, request } = start("download");
try {
const content = await operations.downloadSourceFile(currentId, { signal: controller.signal });
if (!finish(request, controller)) return;
const bytes = content.data.buffer.slice(
content.data.byteOffset,
content.data.byteOffset + content.data.byteLength,
) as ArrayBuffer;
const url = URL.createObjectURL(new Blob([bytes], { type: content.content_type }));
try {
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = safeDownloadFilename(metadata, currentId);
anchor.rel = "noopener";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
} finally {
URL.revokeObjectURL(url);
}
setNotice({ tone: "success", text: `Downloaded ${formatFileSize(content.bytes)} after validating the complete binary response.` });
} catch (error) {
if (!current(request, controller) || isAbort(error)) return;
finish(request, controller);
setNotice({ tone: "error", text: safeFailure(error, "download") });
}
};

const deleteFile = async () => {
if (!currentId) return;
const id = currentId;
Expand Down Expand Up @@ -361,7 +325,7 @@ export function SourceFilesPanel({
<header>
<div>
<FileUp size={15} strokeWidth={1.5} aria-hidden="true" />
<div><h2 id="source-files-heading">Source Files</h2><p>Project-owned upload, metadata, binary download and delete</p></div>
<div><h2 id="source-files-heading">Source Files</h2><p>Project-owned upload, metadata, Workspace copy and delete</p></div>
</div>
<span>{environmentFilesEnabled ? "512 MiB source · 50 MiB destination" : "512 MiB source"}</span>
</header>
Expand Down Expand Up @@ -399,9 +363,9 @@ export function SourceFilesPanel({
</label>
<div className="source-files-actions">
<button className="button outline" type="button" onClick={() => void retrieve()} disabled={busy || !currentId}><Search size={12} />Retrieve</button>
<button className="button outline" type="button" onClick={() => void download()} disabled={busy || !currentId}><Download size={12} />Download</button>
<button className="button outline danger" type="button" onClick={() => void deleteFile()} disabled={busy || !currentId}><Trash2 size={12} />Delete once</button>
</div>
<p>Uploaded Source Files cannot be downloaded directly. Copy them to a Workspace for execution.</p>
<p>Core has no Source Files list API. Refreshing or reopening this page requires the ID again.</p>
</div>
</div>
Expand Down Expand Up @@ -463,7 +427,7 @@ export function SourceFilesPanel({
/>
</label>
{!destinationValid ? <p className="source-files-validation">Use one canonical absolute file path beneath <code>/workspace/</code>; parent traversal and trailing slashes are rejected.</p> : null}
{metadata && metadata.bytes > maxDestinationBytes ? <p className="source-files-validation">This Source File is retained and downloadable, but it exceeds the 50 MiB destination-copy limit.</p> : null}
{metadata && metadata.bytes > maxDestinationBytes ? <p className="source-files-validation">This Source File is retained, but it exceeds the 50 MiB destination-copy limit.</p> : null}
<button
className="button primary"
type="button"
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ paths start at `/vaults`, not `/agents/vaults`.
| Resource | Upstream operations | Current coverage |
| --- | --- | --- |
| Root reusable Agents | create, retrieve, update, list, delete | Partial create/retrieve/update/list/delete and Session references; configuration/error gaps remain |
| Skills and Versions | create, retrieve, update default, list, delete, content | [Tenant-owned encrypted bundles and hosted references](environment-templates.md); qualified upload limits and unresolved hosted semantics are recorded explicitly |
| Skills and Versions | create, retrieve, update default, list, delete, content | [Tenant-owned encrypted bundles and hosted references](environment-templates.md); [default metadata/content and deletion evidence](file-resource-semantics.md), qualified upload limits and unresolved semantics |
| sessions | create, retrieve, update, list, delete | Create (ordinary/live), retrieve, list with root-Agent filter, metadata-only update, public deletion with owned Docker cleanup; user-managed compute stays caller-owned; general physical cleanup and exact hosted semantics remain open |
| sessions.events | create, stream | Text/cancel/function-result admission and live events; function-action state snapshots supported |
| sessions.turns | retrieve, list | Implemented reads; lifecycle conformance still partial |
Expand Down
5 changes: 3 additions & 2 deletions contracts/agents-api/environment-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ preconfigured Environment ID, model-input text, and two private caller token
sources (`token_env` or `token_file`). The invoking native fixture supplies an
existing `uploads` directory and a staging symlink rejection probe, verifies exact
installed hashes, and has a real model consume source-copied text after source
deletion. Its source fixture also streams a 512 MiB upload/download and verifies
that it cannot bypass the smaller destination bound. This distinction
deletion. Its source fixture also streams a 512 MiB upload, checks public download
denial, and verifies that it cannot bypass the smaller destination bound. Internal
large-object streaming integrity has separate PostgreSQL tests. This distinction
keeps private setup separate from public hosted creation acceptance. Mechanism tests
exercise detached/unknown outcomes and durable gates independently of model output.
12 changes: 6 additions & 6 deletions contracts/agents-api/environment-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -899,9 +899,9 @@ index under `~/.parsar/remediation/20260921/template-capabilities-design/`.

The current upload profile accepts at most 500 regular files, 5 MiB compressed
and 20 MiB expanded per bundle. These are qualified implementation limits, not
published protocol maxima. Exact hosted error parity,
unversioned content selection, top-level metadata across version changes and
last/default/latest deletion semantics remain recorded gaps. Current resource
behavior selects default for unversioned content, preserves initial top-level
metadata, rejects default-version deletion and never reuses version numbers.
These choices are not verified upstream guarantees.
published protocol maxima. [File resource qualification](file-resource-semantics.md)
records default-selected unversioned content and descriptive metadata, default
deletion rejection with multiple versions, and nondefault latest pointer fallback.
Core updates the default pointer and top-level name/description atomically, while
concrete version bytes and previously frozen Sessions remain immutable. Last-version
deletion, version-number reuse and complete errors/visibility timing remain gaps.
Loading
Loading