feat(workspace-config): project grant-bound logical settings through WIP

This commit is contained in:
2026-10-06 06:56:51 +09:00
parent 7a96c42130
commit 0a471242cb
44 changed files with 8159 additions and 72 deletions
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -269,6 +269,107 @@ export type ConfigCommitRequest = {
entrypoints: Array<string>;
};
export type WorkspaceConfigAccess = "read_only" | "read_write";
export type WorkspaceConfigNodeKind = "directory" | "file" | "missing";
export type WorkspaceConfigFailureClassification = "not_committed" | "unknown";
export type WorkspaceConfigAttachRequest = {
alias: string | null;
access: WorkspaceConfigAccess | null;
};
export type WorkspaceConfigAttachment = {
workspace_id: string;
connection_id: string;
alias: string;
working_directory_id: string;
access: WorkspaceConfigAccess;
name: string;
purpose: string;
content_path: string;
already_attached: boolean;
};
export type WorkspaceConfigCurrentResponse = WorkspaceConfigAttachment | null;
export type WorkspaceConfigObserveRequest = {
connection_id: string;
paths: Array<string>;
depth: number;
};
export type WorkspaceConfigNode = {
path: string;
kind: WorkspaceConfigNodeKind;
validator: string;
digest: string | null;
content_type: ConfigContentType | null;
operations: Array<string>;
};
export type WorkspaceConfigObserveResponse = {
connection_id: string;
validator: string;
revision: number;
digest: string;
/**
* Canonical commit entrypoints from this exact metadata snapshot.
*/
entrypoints: Array<string>;
nodes: Array<WorkspaceConfigNode>;
};
export type WorkspaceConfigReadRequest = {
connection_id: string;
path: string;
validator: string;
};
export type WorkspaceConfigReadResponse = {
path: string;
content: string;
content_type: ConfigContentType;
digest: string;
validator: string;
};
export type WorkspaceConfigCommitRequest = {
connection_id: string;
validator: string;
request: ConfigCommitRequest;
};
export type WorkspaceConfigCommitResponse = {
validator: string;
revision: number;
digest: string;
};
export type WorkspaceConfigApiError = {
status: number;
code: string;
message: string;
classification: WorkspaceConfigFailureClassification;
};
export type WorkspaceConfigGrantCreateRequest = {
runtime_id: string;
worker_id: string;
access: WorkspaceConfigAccess;
};
export type WorkspaceConfigGrantResponse = {
grant_id: string;
workspace_id: string;
runtime_id: string;
worker_id: string;
working_directory_id: string;
access: WorkspaceConfigAccess;
revoked: boolean;
};
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -79,12 +79,14 @@ export type WorkerWorkspaceSummary = { identity: string; visibility: string; wor
export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; };
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryMaterializerKind = "logical_workspace_config" | "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectorySource = { access: WorkspaceConfigAccess; content_path: string; kind: "workspace_config"; purpose: string; } | { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
export type WorkspaceConfigAccess = "read_only" | "read_write";
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -203,16 +203,18 @@ export type WorkerWorkspaceSummary = { identity: string; visibility: string; wor
export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; };
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryMaterializerKind = "logical_workspace_config" | "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectorySource = { access: WorkspaceConfigAccess; content_path: string; kind: "workspace_config"; purpose: string; } | { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
export type WorkspaceConfigAccess = "read_only" | "read_write";
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
export type WorkspaceRuntimeBindingSummary = { connection_state: RuntimeConnectionDisplayState; revision: number; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_generation?: number | null; workspace_key_id?: string | null; };
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -27,7 +27,7 @@ export type WorkingDirectoryDetailResponse = { diagnostics: Array<Diagnostic>; i
export type WorkingDirectoryListResponse = { diagnostics: Array<Diagnostic>; items: Array<WorkingDirectorySummary>; workspace_id: string; };
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryMaterializerKind = "logical_workspace_config" | "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
@@ -37,8 +37,10 @@ export type WorkingDirectoryRemovalRequest = { reason: string; };
export type WorkingDirectoryRemovalResponse = { disposition: WorkingDirectoryRemovalDisposition; failure_category?: string | null; retryable: boolean; working_directory_id: string; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectorySource = { access: WorkspaceConfigAccess; content_path: string; kind: "workspace_config"; purpose: string; } | { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
export type WorkspaceConfigAccess = "read_only" | "read_write";
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:47064776569f7f208533f24d56ac80c0db6055d508ab29fb0a780c2c66387081)
// Input contract: 0.1.0 (sha256:6001e9f7273288195fb5a9859e4a5932a521d2f2e7c3a9ecd8ec660fd6a37779)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -85,16 +85,18 @@ export type WorkerWorkspaceSummary = { identity: string; visibility: string; wor
export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; };
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryMaterializerKind = "logical_workspace_config" | "runtime_git_clone" | "client_hosted_external";
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
export type WorkingDirectoryRepositoryOption = { default_selector?: string | null; repository_key: string; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectorySource = { access: WorkspaceConfigAccess; content_path: string; kind: "workspace_config"; purpose: string; } | { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
export type WorkspaceConfigAccess = "read_only" | "read_write";
export type WorkspaceWorkerFeatureConnectionsRequest = { subjektiv?: SubjektivWorkerConnectionRequest | null; };
@@ -131,6 +131,7 @@ export function parseWorkingDirectorySummary(
materializer_kind: enumField(record, "materializer_kind", [
"runtime_git_clone",
"client_hosted_external",
"logical_workspace_config",
]),
status: enumField(record, "status", [
"active",
@@ -177,10 +178,27 @@ export function parseWorkingDirectorySummary(
function parseWorkingDirectorySource(value: unknown): WorkingDirectorySource {
const source = exactRecord(
value,
new Set(["kind", "repository_key", "grant_id", "grant_permissions"]),
new Set(["kind", "repository_key", "grant_id", "grant_permissions", "access", "content_path", "purpose"]),
"Workdir source",
);
const kind = stringField(source, "kind");
if (kind === "workspace_config") {
if (source.repository_key !== undefined || source.grant_id !== undefined || source.grant_permissions !== undefined) {
throw new Error("Logical config source must not contain repository or External authority");
}
const access = stringField(source, "access");
if (access !== "read_only" && access !== "read_write") {
throw new Error("Logical config access has an unsupported value");
}
const content_path = stringField(source, "content_path");
if (content_path !== "/workspace-config") {
throw new Error("Logical config must reference its WIP content entrance");
}
return { kind, access, content_path, purpose: stringField(source, "purpose") };
}
if (source.access !== undefined || source.content_path !== undefined || source.purpose !== undefined) {
throw new Error("Filesystem Workdir source must not contain logical config metadata");
}
if (kind === "repository") {
if (
source.grant_id !== undefined || source.grant_permissions !== undefined
@@ -363,6 +363,72 @@ test("restored invocation chips fetch declarative descriptors and stay editable
expect(snapshot.segments[0].kind).toBe("feature_invoke");
});
// Fixture of the Workspace config Feature declaration, consumed by the same
// generic composer as other Features. Backend/WIP acceptance is tested in Rust;
// this test proves the real input component's selection and typed-submit edge.
const workspaceConfigDescriptor: FeatureInvocationDescriptor = {
identity: "builtin:workspace-config/attach", name: "workspace-config", aliases: [],
display_name: "Workspace config", description: "Select the logical Workspace config attachment",
syntax: "parenthesized", arguments: [{ name: "access", position: 0, required: false,
value_type: { kind: "enum", values: ["effective", "read_only", "read_write"] },
completion: { kind: "none" } }],
};
test("workspace-config completion prepares a typed invocation without consuming the request prose", async () => {
const onsubmit = vi.fn();
const resolveFeatureCompletions = vi.fn(async () => [
{ value: workspaceConfigDescriptor.name, invocation: workspaceConfigDescriptor },
]);
const ui = render(ComposerInput, { historyScope: "workspace-config-selected", onsubmit,
resolveFeatureCompletions });
const cm = editor(ui.container);
type(cm, "/workspace-con");
await ui.findByRole("option");
await fireEvent.keyDown(cm.contentDOM, { key: "Enter" });
expect(onsubmit).not.toHaveBeenCalled();
const suffix = ') このWorkspaceのモデル設定を変更して';
cm.dispatch({ changes: { from: cm.state.doc.length, insert: suffix },
selection: { anchor: cm.state.doc.length + suffix.length } });
await waitFor(() => expect(ui.container.querySelectorAll(".composer-typed-chip")).toHaveLength(1));
const snapshot = ui.component.snapshot();
expect(snapshot.segments).toEqual([
{ kind: "feature_invoke", invocation: {
invocation_id: expect.any(String), identity: workspaceConfigDescriptor.identity,
name: workspaceConfigDescriptor.name, arguments: [],
} },
{ kind: "text", content: " このWorkspaceのモデル設定を変更して" },
]);
await fireEvent.keyDown(cm.contentDOM, { key: "Enter", ctrlKey: true });
expect(onsubmit).toHaveBeenCalledOnce();
// Sending is not string scanning: identity and arguments survive intact.
expect(ui.component.snapshot().segments).toEqual(snapshot.segments);
});
test("workspace-config text alone is not invoked and a selected intent is removable", async () => {
const ui = render(ComposerInput, { historyScope: "workspace-config-literal",
resolveFeatureCompletions: async () => [{ value: workspaceConfigDescriptor.name, invocation: workspaceConfigDescriptor }] });
const cm = editor(ui.container);
const prose = "/workspace-config このWorkspaceのモデル設定を変更して";
ui.component.restoreSegments([{ kind: "text", content: prose }]);
expect(ui.component.snapshot().segments).toEqual([{ kind: "text", content: prose }]);
ui.component.clear();
type(cm, "/workspace-con");
await ui.findByRole("option");
await fireEvent.keyDown(cm.contentDOM, { key: "Enter" });
cm.dispatch({ changes: { from: cm.state.doc.length, insert: '"read_only")' },
selection: { anchor: cm.state.doc.length + '"read_only")'.length } });
await waitFor(() => expect(ui.container.querySelectorAll(".composer-typed-chip")).toHaveLength(1));
expect(ui.component.snapshot().segments[0]).toMatchObject({ kind: "feature_invoke", invocation: {
arguments: [{ name: "access", value: { kind: "string", value: "read_only" } }],
} });
// happy-dom's focused DOM selection can reenter CodeMirror during range
// replacement. Use the existing component suite's unfocused range fixture.
cm.contentDOM.blur();
cm.dispatch({ selection: { anchor: 0, head: cm.state.doc.length } });
await fireEvent.keyDown(cm.contentDOM, { key: "Backspace" });
expect(ui.component.snapshot().segments).toEqual([]);
});
test("attachment adapter uses descriptor capability rather than a hardcoded slash name", async () => {
const onclientadapter = vi.fn();
const adapter = { ...invocationDescriptor, name: "send-file", arguments: [], client_adapter: "attachment" as const };
@@ -9,6 +9,18 @@ declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
};
Deno.test("logical config attachments are never offered as process working directories", () => {
const config = { working_directory_id: "config-1", display_name: "Workspace config",
source: { kind: "workspace_config" as const, access: "read_write" as const, content_path: "/workspace-config", purpose: "Configuration" },
materializer_kind: "logical_workspace_config" as const, status: "active" as const, cleanliness: "clean" };
const form = defaultWorkerLaunchForm({ ...options, working_directories: [config] }, emptyForm({
workdir_attachments: [{ alias: "config", working_directory_id: "config-1", relative_cwd: "" }],
}));
if (form.workdir_attachments.some((attachment) => attachment.working_directory_id === "config-1")) {
throw new Error("logical config was converted to a process cwd attachment");
}
});
Deno.test("defaultWorkerLaunchForm uses the Backend-published defaults and initial Workdir", () => {
const form = defaultWorkerLaunchForm(
options,
@@ -41,6 +41,7 @@ export function defaultWorkerLaunchForm(
const availableWorkingDirectories =
options?.working_directories.filter((directory) =>
directory.status === "active" &&
directory.source.kind !== "workspace_config" &&
(directory.source.kind === "external_grant" ||
directory.cleanliness === "clean") &&
directory.occupied_by == null
@@ -47,6 +47,18 @@ function attachment(
};
}
Deno.test("logical config sidebar label is not fabricated as a repository revision", () => {
const meta = sidebarWorkdirMeta([attachment("workspace-config", {
display_name: "Workspace configuration",
source: { kind: "workspace_config", access: "read_write", content_path: "/workspace-config", purpose: "Configuration" },
materializer_kind: "logical_workspace_config",
})]);
assertEquals(meta.text, "Workspace configuration");
assert(!meta.text.includes("unknown-repo"), "config must not be labeled as a repository");
assert(!meta.text.includes("editable"), "catalog grant ceiling must not claim effective connection editability");
assert(meta.details.includes("workspace-config — Workspace configuration"), "logical alias remains visible");
});
Deno.test("Worker sidebar Workdir label prefers the current branch selector", () => {
const meta = sidebarWorkdirMeta([
attachment("checkout", {
@@ -27,6 +27,11 @@ export function sidebarWorkdirMeta(
function attachmentLabel(attachment: SidebarWorkdirAttachment): string {
const workdir = attachment.working_directory;
if (workdir?.source.kind === "workspace_config") {
// Catalog access is the grant ceiling, not this connection's effective
// restriction. The invocation result reports effective attachment access.
return clean(workdir.display_name) ?? "Workspace config";
}
if (workdir?.source.kind === "external_grant") {
return clean(workdir.display_name) ?? "external";
}
+20
View File
@@ -23,6 +23,26 @@ const summary = {
},
};
Deno.test("logical config Workdir source preserves WIP metadata without filesystem authority", () => {
const source = { kind: "workspace_config", access: "read_only", content_path: "/workspace-config", purpose: "Workspace configuration" };
const response = (source: unknown) => ({ workspace_id: "workspace-a", item: {
working_directory_id: "config-1", source, materializer_kind: "logical_workspace_config", status: "active",
}, diagnostics: [] });
const detail = parseWorkingDirectoryDetailResponse(response(source));
if (JSON.stringify(detail.item.source) !== JSON.stringify(source) || detail.runtime_id !== undefined) {
throw new Error("logical config identity was lost or acquired a fake Runtime");
}
for (const invalid of [
{ ...source, access: "command" }, { ...source, content_path: "/home/config" },
{ ...source, grant_id: "external-grant" }, { ...source, repository_key: "main" },
{ ...source, token: "secret" }, { kind: "repository", repository_key: "main", access: "read_write" },
]) {
let rejected = false;
try { parseWorkingDirectoryDetailResponse(response(invalid)); } catch { rejected = true; }
if (!rejected) throw new Error("mixed or unsafe source metadata was accepted");
}
});
Deno.test("Workdir REST validation accepts the generated list and create contracts", () => {
const list = parseWorkingDirectoryListResponse({
workspace_id: "workspace-a",