fix: preserve external command attachment authority

This commit is contained in:
2026-09-29 14:41:36 +09:00
parent 4ab2a2b88a
commit e9e0368db4
21 changed files with 955 additions and 80 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts",
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts",
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview",
@@ -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:e0407f2ae3f1d454d05543627b87eff8eb001b5a90a900ca5af91d7bb492c3be)
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
// 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
@@ -15,6 +15,8 @@ export type Diagnostic = { code: string; message: string; severity: DiagnosticSe
export type DiagnosticSeverity = "info" | "warning" | "error";
export type ExternalWorkdirPermissions = { command: boolean; read: boolean; write: boolean; };
export type GitCommitSummary = { author_date: string; author_email: string; author_name: string; hash: string; parents: Array<string>; refs: Array<string>; short_hash: string; summary: string; };
export type GitRemoteSummary = { fetch_url: string; name: string; };
@@ -73,7 +75,7 @@ export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState;
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; };
@@ -83,7 +85,7 @@ export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hos
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; kind: "external_grant"; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
@@ -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:e0407f2ae3f1d454d05543627b87eff8eb001b5a90a900ca5af91d7bb492c3be)
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
// 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
@@ -27,6 +27,8 @@ export type DiagnosticSeverity = "info" | "warning" | "error";
export type ExecuteRuntimeCleanupRequest = { confirm_dirty_discard_target_ids?: Array<string>; expected_plan_digest: string; expected_plan_revision: string; workdir_target_ids?: Array<string>; worker_target_ids?: Array<string>; };
export type ExternalWorkdirPermissions = { command: boolean; read: boolean; write: boolean; };
export type RemoveRuntimeRequest = { expected_binding_revision: number; operation_id: string; };
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
@@ -163,7 +165,7 @@ export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState;
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; };
@@ -173,7 +175,7 @@ export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hos
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; kind: "external_grant"; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
@@ -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:e0407f2ae3f1d454d05543627b87eff8eb001b5a90a900ca5af91d7bb492c3be)
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
// 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
@@ -37,7 +37,7 @@ 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; kind: "external_grant"; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
@@ -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:e0407f2ae3f1d454d05543627b87eff8eb001b5a90a900ca5af91d7bb492c3be)
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
// 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
@@ -19,6 +19,8 @@ export type Diagnostic = { code: string; message: string; severity: DiagnosticSe
export type DiagnosticSeverity = "info" | "warning" | "error";
export type ExternalWorkdirPermissions = { command: boolean; read: boolean; write: boolean; };
export type PasteArtifactAvailability = "available" | "unavailable" | "integrity_failed";
export type PasteArtifactMediaType = "text_plain_utf8";
@@ -67,7 +69,7 @@ export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState;
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; };
@@ -79,7 +81,7 @@ export type WorkingDirectoryOccupancy = { display_name: string; linked_at: strin
export type WorkingDirectoryRepositoryOption = { default_selector?: string | null; repository_key: string; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; };
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; grant_permissions: ExternalWorkdirPermissions; kind: "external_grant"; };
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
@@ -1,5 +1,6 @@
import type {
Diagnostic,
ExternalWorkdirPermissions,
WorkingDirectoryCleanupTarget,
WorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse,
@@ -176,13 +177,15 @@ export function parseWorkingDirectorySummary(
function parseWorkingDirectorySource(value: unknown): WorkingDirectorySource {
const source = exactRecord(
value,
new Set(["kind", "repository_key", "grant_id"]),
new Set(["kind", "repository_key", "grant_id", "grant_permissions"]),
"Workdir source",
);
const kind = stringField(source, "kind");
if (kind === "repository") {
if (source.grant_id !== undefined) {
throw new Error("Repository Workdir source must not contain grant_id");
if (source.grant_id !== undefined || source.grant_permissions !== undefined) {
throw new Error(
"Repository Workdir source must not contain External grant authority",
);
}
return {
kind,
@@ -198,11 +201,31 @@ function parseWorkingDirectorySource(value: unknown): WorkingDirectorySource {
return {
kind,
grant_id: stringField(source, "grant_id"),
grant_permissions: parseExternalWorkdirPermissions(
source.grant_permissions,
"External Workdir grant permissions",
),
};
}
throw new Error("Workdir source.kind has an unsupported value");
}
export function parseExternalWorkdirPermissions(
value: unknown,
label = "External Workdir permissions",
): ExternalWorkdirPermissions {
const permissions = exactRecord(
value,
new Set(["read", "write", "command"]),
label,
);
return {
read: booleanField(permissions, "read", label),
write: booleanField(permissions, "write", label),
command: booleanField(permissions, "command", label),
};
}
function parseCleanupTarget(value: unknown): WorkingDirectoryCleanupTarget {
const record = exactRecord(
value,
@@ -260,6 +283,18 @@ function stringField(record: Record<string, unknown>, key: string): string {
return value;
}
function booleanField(
record: Record<string, unknown>,
key: string,
label: string,
): boolean {
const value = record[key];
if (typeof value !== "boolean") {
throw new Error(`${label}.${key} must be a boolean`);
}
return value;
}
function arrayField(record: Record<string, unknown>, key: string): unknown[] {
const value = record[key];
if (!Array.isArray(value)) throw new Error(`${key} must be an array`);
@@ -96,6 +96,48 @@ Deno.test("Worker summary parser enforces observation freshness", () => {
assertEquals(observedStopped.state, "stopped");
});
Deno.test("Worker summary parser preserves attachment-effective External permissions", () => {
const attachment = {
alias: "sessions",
effective_permissions: { read: false, write: false, command: true },
working_directory: {
working_directory_id: "external-1",
display_name: "Sessions",
source: {
kind: "external_grant",
grant_id: "grant-1",
grant_permissions: { read: true, write: false, command: true },
},
materializer_kind: "client_hosted_external",
status: "active",
},
};
const parsed = parseWorkerSummary({
...worker,
resource_key: "W-1",
availability: "observed",
workdir_attachments: [attachment],
});
assertEquals(
parsed.workdir_attachments?.[0]?.effective_permissions,
attachment.effective_permissions,
);
assertThrows(
() =>
parseWorkerSummary({
...worker,
resource_key: "W-1",
availability: "observed",
workdir_attachments: [{
alias: attachment.alias,
working_directory: attachment.working_directory,
}],
}),
Error,
"effective_permissions",
);
});
Deno.test("Worker launch options parser accepts the generated wire shape", () => {
const parsed = parseWorkerLaunchOptionsResponse({
workspace_id: "workspace-a",
@@ -24,7 +24,10 @@ import type {
WorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api";
import type { Segment } from "$lib/generated/protocol";
import { parseWorkingDirectorySummary } from "$lib/workspace/api/workdirs";
import {
parseExternalWorkdirPermissions,
parseWorkingDirectorySummary,
} from "$lib/workspace/api/workdirs";
const DIAGNOSTIC_SEVERITIES = new Set<DiagnosticSeverity>([
"info",
@@ -316,9 +319,13 @@ function workerWorkdirAttachment(
label: string,
): WorkerWorkdirAttachmentSummary {
const item = record(value, label);
exact(item, ["alias", "working_directory"], label);
exact(item, ["alias", "effective_permissions", "working_directory"], label);
return {
alias: string(item.alias, `${label}.alias`),
effective_permissions: parseExternalWorkdirPermissions(
item.effective_permissions,
`${label}.effective_permissions`,
),
working_directory: parseWorkingDirectorySummary(item.working_directory),
};
}
@@ -0,0 +1,12 @@
import type { ExternalWorkdirPermissions } from "$lib/generated/workdir-api";
/** Render independent Workdir authority categories in a stable, non-conflating order. */
export function formatWorkdirPermissions(
permissions: ExternalWorkdirPermissions,
): string {
const categories: string[] = [];
if (permissions.read) categories.push("READ");
if (permissions.write) categories.push("WRITE");
if (permissions.command) categories.push("COMMAND");
return categories.length > 0 ? categories.join(" · ") : "NONE";
}
@@ -32,7 +32,11 @@ Deno.test("defaultWorkerLaunchForm preserves an available read-only External Wor
const external = {
working_directory_id: "external-1",
display_name: "Session analysis",
source: { kind: "external_grant" as const, grant_id: "grant-1" },
source: {
kind: "external_grant" as const,
grant_id: "grant-1",
grant_permissions: { read: true, write: false, command: false },
},
materializer_kind: "client_hosted_external" as const,
status: "active" as const,
cleanliness: "unknown",
@@ -111,7 +111,11 @@ Deno.test("Worker sidebar Workdir label uses an External Workdir display name wi
const meta = sidebarWorkdirMeta([
attachment("sessions", {
display_name: "Session analysis",
source: { kind: "external_grant", grant_id: "grant-1" },
source: {
kind: "external_grant",
grant_id: "grant-1",
grant_permissions: { read: true, write: false, command: false },
},
materializer_kind: "client_hosted_external",
cleanliness: "unknown",
}),
@@ -7,6 +7,7 @@
parseWorkerRetentionResponse,
} from '$lib/workspace/api/runtime-workers';
import { workerHref } from '$lib/workspace/resource-links';
import { formatWorkdirPermissions } from '$lib/workspace/settings/workdir-permissions';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
@@ -149,7 +150,7 @@
function workerDirectory(worker: Worker): string {
const attachments = worker.workdir_attachments ?? [];
if (attachments.length === 0) return '—';
return attachments.map(({ alias, working_directory: directory }) => {
return attachments.map(({ alias, effective_permissions, working_directory: directory }) => {
const repositoryKey = directory.source.kind === 'repository'
? directory.source.repository_key
: null;
@@ -157,6 +158,9 @@
? data.repositories?.items.find((repository) => repository.repository_key === repositoryKey)?.provider
: null;
const label = directory.display_name ?? repositoryKey ?? 'External Workdir';
if (directory.source.kind === 'external_grant') {
return `${alias}: ${label} · ${formatWorkdirPermissions(effective_permissions)}`;
}
return `${alias}: ${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
}).join(', ');
}
@@ -12,6 +12,7 @@
parseWorkerLaunchOptionsResponse,
} from '$lib/workspace/api/workers';
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
import { formatWorkdirPermissions } from '$lib/workspace/settings/workdir-permissions';
import {
buildCreateWorkspaceWorkerRequest,
defaultWorkerLaunchForm,
@@ -38,6 +39,9 @@
? data.repositories?.items.find((repository) => repository.repository_key === repositoryKey)?.provider
: null;
const label = directory.display_name ?? repositoryKey ?? 'External Workdir';
if (directory.source.kind === 'external_grant') {
return `${label} · External · ${formatWorkdirPermissions(directory.source.grant_permissions)}`;
}
return `${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
}
+51 -1
View File
@@ -53,7 +53,11 @@ Deno.test("Workdir REST validation accepts External grant sources without Runtim
item: {
working_directory_id: "external-1",
display_name: "Session analysis",
source: { kind: "external_grant", grant_id: "grant-1" },
source: {
kind: "external_grant",
grant_id: "grant-1",
grant_permissions: { read: true, write: false, command: true },
},
materializer_kind: "client_hosted_external",
status: "active",
cleanliness: "unknown",
@@ -66,6 +70,52 @@ Deno.test("Workdir REST validation accepts External grant sources without Runtim
if (detail.item.source.kind !== "external_grant") {
throw new Error("External Workdir source was not preserved");
}
if (
!detail.item.source.grant_permissions.read ||
detail.item.source.grant_permissions.write ||
!detail.item.source.grant_permissions.command
) {
throw new Error("External Workdir grant permissions were not preserved");
}
});
Deno.test("Workdir REST validation rejects missing or misplaced External grant permissions", () => {
for (
const source of [
{ kind: "external_grant", grant_id: "grant-1" },
{
kind: "external_grant",
grant_id: "grant-1",
grant_permissions: { read: true, write: false, command: "yes" },
},
{
kind: "repository",
repository_key: "main",
grant_permissions: { read: true, write: false, command: false },
},
]
) {
let rejected = false;
try {
parseWorkingDirectoryDetailResponse({
workspace_id: "workspace-a",
item: {
working_directory_id: "invalid-1",
source,
materializer_kind: source.kind === "repository"
? "runtime_git_clone"
: "client_hosted_external",
status: "active",
},
diagnostics: [],
});
} catch {
rejected = true;
}
if (!rejected) {
throw new Error(`invalid External permission projection was accepted: ${JSON.stringify(source)}`);
}
}
});
Deno.test("Workdir REST validation rejects stale response JSON", () => {
@@ -0,0 +1,22 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import { formatWorkdirPermissions } from "../src/lib/workspace/settings/workdir-permissions.ts";
Deno.test("Workdir permissions render independent categories in stable order", () => {
const cases = [
[{ read: true, write: false, command: false }, "READ"],
[{ read: true, write: true, command: false }, "READ · WRITE"],
[{ read: false, write: false, command: true }, "COMMAND"],
[{ read: true, write: false, command: true }, "READ · COMMAND"],
[{ read: true, write: true, command: true }, "READ · WRITE · COMMAND"],
] as const;
for (const [permissions, expected] of cases) {
const actual = formatWorkdirPermissions(permissions);
if (actual !== expected) {
throw new Error(`expected ${expected}, got ${actual}`);
}
}
});