Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
30 changes: 30 additions & 0 deletions docs/architecture/15-runtime-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,36 @@ untrusted project code. Configured storage failures do not downgrade to local hi
The provider, handle, record types, and handle factory are exported by
`veryfront/platform`.

## API-derived dependency metadata history

`FileSystemAdapter.readDependencyMetadataHistory()` is an optional read-only
capability, separate from `RuntimeAdapter.dependencySnapshotStore`. The Veryfront
filesystem implements it through the existing authenticated project API and the
same project-scoped token used for file reads. It does not obtain shared internal
credentials or expose a snapshot publication endpoint to project code.

Before a dependency-resolution write changes `package.json`, the API acknowledges
storage of its own observed prior dependency map, including an absent file as an
empty map. The read endpoint returns at most 16 recent maps within a 1 MiB response
limit. Metadata publication has a 960 KiB admission bound so an acknowledged
individual record fits the response. The project and canonical branch scope are
derived by the API, with the requested branch retained in the response for matching.

A cold renderer consults this capability only after local history misses and the
current dependency key differs. It combines a prior raw map with its current
captured React/Veryfront configuration and accepts it only if the exact requested
key matches. Scope mismatch, corrupt data, outages and expired records fail closed.
Recovered data keeps its acknowledged expiry and never becomes current writeback
authority. Reader methods are captured before use and remain associated with their
original source when file reads are wrapped for tracking.

This does not weaken the shared snapshot store's acknowledged publication contract:
an explicitly configured store never falls back to this metadata reader. Standalone
filesystems do not require an API or shared backend. Direct package edits, concurrent
configuration changes, history outside the bounded read window, and historical data
predating activation can remain unavailable. They still return a conflict rather
than interpreting an old key using current dependencies.

## Change checks

- Update [support matrix](./20-support-matrix.md) when runtime support changes.
Expand Down
37 changes: 19 additions & 18 deletions docs/architecture/20-support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,25 @@ These are the runtime capability profiles modeled by the framework.
This matrix separates open-core framework support from capabilities that depend
on a backing API or cloud bootstrap.

| Capability | Current support shape | Notes |
| --------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Routing, rendering, middleware, API routes | Open-core | Core framework capability. |
| Shared dependency snapshot history | Open-core with an explicit host provider | Requires native proxy detection on Deno, Node, or Bun. Hosts without it retain default process-local history. |
| App MCP server | Open-core on runtimes that can host it | Not available on constrained runtimes like Cloudflare Workers. |
| Internal AG-UI transport | Open-core runtime surface | Separate from the app MCP contract. |
| Direct provider integrations (`openai`, `anthropic`, `google`, local) | Open-core with provider credentials/runtime setup | Depends on the selected provider configuration. |
| Extension contracts (auth, bundler, CSS, parser, observability, etc.) | Open-core | First-party `@veryfront/ext-*` packages provide implementations. |
| Workflow engine (in-memory and Redis backends) | Open-core | In-memory, Redis, and process run execution work without Kubernetes. |
| Discovery (tools, agents, workflows, prompts, resources, skills) | Open-core | Convention-based file-system discovery at server startup. |
| Veryfront Cloud model routing | Requires Veryfront Cloud bootstrap | Depends on project/auth context and cloud gateway configuration. |
| Veryfront Cloud blob storage | Requires Veryfront Cloud bootstrap | Uses project-scoped cloud upload APIs. |
| Veryfront Cloud agent service | Requires Veryfront Cloud bootstrap | Hosted agent execution with project steering and runtime system messages. |
| Runs client | Requires backing API/service layer | Exposed as SDK/API surface for task, workflow, and agent execution. |
| Sandbox | Requires backing API/service layer | Depends on authenticated sandbox session APIs. |
| Remote integration tools | Requires backing API/service layer | Tool definitions and execution are fetched per request from the configured API layer. |
| Local catalog integration tools | Open-core with provider credentials/runtime setup | Exact-grant local sources execute supported HTTPS REST endpoints directly. Salesforce also has a dedicated service-account source. |
| Control-plane agent routing | Requires Veryfront Cloud bootstrap | EdDSA-signed request validation for hosted agent orchestration. |
| Capability | Current support shape | Notes |
| --------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Routing, rendering, middleware, API routes | Open-core | Core framework capability. |
| Shared dependency snapshot history | Open-core with an explicit host provider | Requires native proxy detection on Deno, Node, or Bun. Hosts without it retain default process-local history. |
| Prior dependency metadata | Optional Veryfront project API filesystem capability | Read-only recovery for dependency-resolution writebacks; requires exact project/branch and dependency-key matching. Not general shared snapshot storage. |
| App MCP server | Open-core on runtimes that can host it | Not available on constrained runtimes like Cloudflare Workers. |
| Internal AG-UI transport | Open-core runtime surface | Separate from the app MCP contract. |
| Direct provider integrations (`openai`, `anthropic`, `google`, local) | Open-core with provider credentials/runtime setup | Depends on the selected provider configuration. |
| Extension contracts (auth, bundler, CSS, parser, observability, etc.) | Open-core | First-party `@veryfront/ext-*` packages provide implementations. |
| Workflow engine (in-memory and Redis backends) | Open-core | In-memory, Redis, and process run execution work without Kubernetes. |
| Discovery (tools, agents, workflows, prompts, resources, skills) | Open-core | Convention-based file-system discovery at server startup. |
| Veryfront Cloud model routing | Requires Veryfront Cloud bootstrap | Depends on project/auth context and cloud gateway configuration. |
| Veryfront Cloud blob storage | Requires Veryfront Cloud bootstrap | Uses project-scoped cloud upload APIs. |
| Veryfront Cloud agent service | Requires Veryfront Cloud bootstrap | Hosted agent execution with project steering and runtime system messages. |
| Runs client | Requires backing API/service layer | Exposed as SDK/API surface for task, workflow, and agent execution. |
| Sandbox | Requires backing API/service layer | Depends on authenticated sandbox session APIs. |
| Remote integration tools | Requires backing API/service layer | Tool definitions and execution are fetched per request from the configured API layer. |
| Local catalog integration tools | Open-core with provider credentials/runtime setup | Exact-grant local sources execute supported HTTPS REST endpoints directly. Salesforce also has a dedicated service-account source. |
| Control-plane agent routing | Requires Veryfront Cloud bootstrap | EdDSA-signed request validation for hosted agent orchestration. |

## Extension contract matrix

Expand Down
4 changes: 4 additions & 0 deletions src/platform/adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ export interface FileSystemAdapter {
* callers must not carry freshness across a possible context change.
*/
getSourceSnapshotIdentity?(): string | undefined | Promise<string | undefined>;
/** Read trusted prior dependency metadata for this adapter's mutable source scope. */
readDependencyMetadataHistory?(): Promise<
import("./dependency-metadata-history.ts").DependencyMetadataHistory
>;
}

/** A filesystem adapter that advertises genuine bounded byte reads. */
Expand Down
11 changes: 11 additions & 0 deletions src/platform/adapters/dependency-metadata-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface DependencyMetadataHistoryEntry {
readonly dependencies: Readonly<Record<string, string>>;
readonly expiresAt: number;
}

export interface DependencyMetadataHistory {
readonly version: 1;
readonly projectId: string;
readonly branch: string | null;
readonly entries: readonly DependencyMetadataHistoryEntry[];
}
4 changes: 4 additions & 0 deletions src/platform/adapters/fs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ export {
wrapFSAdapter,
} from "./wrapper.ts";
export type { ExtendedFileSystemAdapter } from "./wrapper.ts";
export type {
DependencyMetadataHistory,
DependencyMetadataHistoryEntry,
} from "../dependency-metadata-history.ts";
11 changes: 11 additions & 0 deletions src/platform/adapters/fs/veryfront/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
ResolveFileOptions,
SourceSnapshotFreshnessOptions,
} from "#veryfront/platform/adapters/base.ts";
import type { DependencyMetadataHistory } from "#veryfront/platform/adapters/dependency-metadata-history.ts";
import { VeryfrontApiClient } from "../../veryfront-api-client/index.ts";
import type { Project } from "../../veryfront-api-client/index.ts";
import { FileCache } from "../cache/file-cache.ts";
Expand Down Expand Up @@ -1640,6 +1641,16 @@ export class VeryfrontFSAdapter implements FSAdapter {
return this.#getCurrentSourceSnapshotIdentity();
}

async readDependencyMetadataHistory(): Promise<DependencyMetadataHistory> {
await this.#ensureExactReadInitialized();
const source = this.getEffectiveContentContext();
if (source?.sourceType !== "branch") {
throw new TypeError("Dependency metadata history is available only for branch sources");
}
const branch = source.branch && source.branch !== "main" ? source.branch : null;
return await this.client.readDependencyMetadataHistory(branch);
}

getPokeMetrics(): {
received: number;
invalidationsTriggered: number;
Expand Down
146 changes: 146 additions & 0 deletions src/platform/adapters/fs/veryfront/dependency-metadata-history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import "#veryfront/schemas/_test-setup.ts";

import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { installMockFetch, restoreMockFetch } from "#veryfront/testing/mock-fetch.ts";
import { VeryfrontFSAdapter } from "./adapter.ts";
import { MultiProjectFSAdapter } from "./multi-project-adapter.ts";

const PROJECT_ID = "10000000-1000-4000-8000-100000000001";

function createAdapter(): VeryfrontFSAdapter {
return new VeryfrontFSAdapter({
veryfront: {
apiBaseUrl: "https://api.example.com",
apiToken: "project-token",
projectSlug: "project-slug",
projectId: PROJECT_ID,
cache: { enabled: false },
},
});
}

describe("VeryfrontFSAdapter dependency metadata history", () => {
it("reads the exact active branch scope", async () => {
const adapter = createAdapter();
adapter.setContentContext({
sourceType: "branch",
projectSlug: "project-slug",
branch: "feature/exact",
});
const calls: Array<string | null> = [];
const history = {
version: 1 as const,
projectId: PROJECT_ID,
branch: "feature/exact",
entries: [] as const,
};
adapter.getClient().readDependencyMetadataHistory = (branch) => {
calls.push(branch);
return Promise.resolve(history);
};

assertEquals(await adapter.readDependencyMetadataHistory(), history);
assertEquals(calls, ["feature/exact"]);
});

it("isolates concurrent multi-project and branch request contexts", async () => {
const adapter = new MultiProjectFSAdapter({
veryfront: {
apiBaseUrl: "https://api.example.com",
proxyMode: true,
cache: { enabled: false },
},
});
const requests: Array<{ project: string; branch: string | null; authorization: string }> = [];
installMockFetch(async (input, init) => {
const url = new URL(String(input));
const project = url.pathname.split("/")[2];
const branch = url.searchParams.get("branch");
const projectId = project === "project-a"
? "10000000-1000-4000-8000-100000000001"
: "20000000-2000-4000-8000-200000000002";
if (url.pathname === `/projects/${project}`) {
return Response.json({ id: projectId, name: project, slug: project });
}
if (url.pathname === `/projects/${project}/files`) {
return Response.json({
data: [],
page_info: { self: null, first: null, next: null, prev: null },
});
}
requests.push({
project,
branch,
authorization: new Headers(init?.headers).get("authorization") ?? "",
});
await new Promise((resolve) => setTimeout(resolve, project === "project-a" ? 10 : 0));
return Response.json({
version: 1,
project_id: projectId,
branch,
entries: [],
});
});

try {
const [a, b] = await Promise.all([
adapter.runWithContext(
"project-a",
"token-a",
() => adapter.readDependencyMetadataHistory(),
"10000000-1000-4000-8000-100000000001",
{ branch: "branch-a" },
),
adapter.runWithContext(
"project-b",
"token-b",
() => adapter.readDependencyMetadataHistory(),
"20000000-2000-4000-8000-200000000002",
{ branch: "branch-b" },
),
]);

assertEquals(a.projectId, "10000000-1000-4000-8000-100000000001");
assertEquals(a.branch, "branch-a");
assertEquals(b.projectId, "20000000-2000-4000-8000-200000000002");
assertEquals(b.branch, "branch-b");
assertEquals(requests.sort((left, right) => left.project.localeCompare(right.project)), [
{ project: "project-a", branch: "branch-a", authorization: "Bearer token-a" },
{ project: "project-b", branch: "branch-b", authorization: "Bearer token-b" },
]);
} finally {
restoreMockFetch();
adapter.dispose();
}
});

it("rejects immutable release and environment sources without falling back to main", async () => {
for (
const context of [
{ sourceType: "release" as const, projectSlug: "project-slug", releaseId: "release-id" },
{
sourceType: "environment" as const,
projectSlug: "project-slug",
environmentName: "production",
releaseId: "release-id",
},
]
) {
const adapter = createAdapter();
adapter.setContentContext(context);
let called = false;
adapter.getClient().readDependencyMetadataHistory = () => {
called = true;
return Promise.reject(new Error("must not call"));
};

await assertRejects(
() => adapter.readDependencyMetadataHistory(),
Error,
"branch sources",
);
assertEquals(called, false);
}
});
});
Loading
Loading