the file browser can actually read a member's home, and plans is gone
"This folder is empty" was a lie. The five seeded directories were sitting there and the platform's readdir raised EACCES: a member's home is 700 and owned by them, which is correct for a shell and locks out the file browser, which runs inside the platform process. /ls caught the error and returned an empty listing, so a refusal looked exactly like data. Two doors, two boundaries, and that is the point rather than a compromise. The terminal and the agent RUN AS the member and the kernel is the boundary there. The file browser acts on the member's behalf from inside the platform, which already applies its own containment and is the owner's process on the owner's machine — it can read anything via sudo regardless. Giving it access describes who is doing the work. Done with named POSIX ACLs, because it has to hold in BOTH directions: a file the platform writes must be editable by the member and vice versa. Mode bits cannot say that — whichever party is neither owner nor group lands in "other", and widening "other" opens the home to every account on the box. A shared group fails the same way, since both parties would have to be in it and that puts every member in a group that can read every other member's home. Two named entries plus `d:` defaults grant exactly two users and are inherited by whatever either side creates, whatever their umask. Verified: platform lists the home, member edits a platform-written file, platform edits a member-written file, and a SECOND member is refused on both ls and cat. /ls now distinguishes EACCES from a missing directory. An empty result is data and must never be how a refusal looks. acl joins the core packages in setup.sh — the alternative is an account that provisions and then cannot list its own home. Also: the file browser's own useTasks/useAgents fired /tasks, /agents and both category endpoints on every render, which is where the last four 403s came from — they are the context menu's Run Task and agent submenus, execution-only. Gated. And plans is deleted: router, screen, routes, dock tile, hook, page title and its capability. It read markdown from <repo>/plans, which does not exist. Fresh-install Permissions is now Files alone, with Terminal to come. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useCapabilities } from 'hooks/useCapabilities';
|
||||
import { type TriggerConfig, groupByCategory, matchesTrigger } from './useTasks';
|
||||
|
||||
export type AgentSummary = {
|
||||
@@ -17,15 +18,21 @@ export type AgentGroup = { category: string; agents: AgentSummary[] };
|
||||
// wrong thing about what happens when you click.
|
||||
export const useAgents = () => {
|
||||
const client = useClient();
|
||||
// Agents are the `items` capability — skills, tools and agents on the owner's disk — and running one
|
||||
// starts a chat session, which is `chat`. Both are execution-only, so a member gets no agent submenu.
|
||||
const { can } = useCapabilities();
|
||||
const allowed = can('items');
|
||||
|
||||
const { data: agents = [] } = useQuery<AgentSummary[]>({
|
||||
queryKey: ['agents'],
|
||||
enabled: allowed,
|
||||
queryFn: () => client.get('/agents'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const { data: categoryOrder = [] } = useQuery<string[]>({
|
||||
queryKey: ['agent-categories'],
|
||||
enabled: allowed,
|
||||
queryFn: () => client.get('/agents/categories'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useCapabilities } from 'hooks/useCapabilities';
|
||||
|
||||
export type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' };
|
||||
|
||||
@@ -70,15 +71,22 @@ export const matchesTrigger = (
|
||||
|
||||
export const useTasks = () => {
|
||||
const client = useClient();
|
||||
// `tasks` is `kind: 'execution'`: a task run executes a script as the server owner. A member browsing
|
||||
// their own files has a file browser, not a task runner — so the context menu simply has no Run Task
|
||||
// submenu, and these two requests are not made. Without the guard they 403'd on every Files render.
|
||||
const { can } = useCapabilities();
|
||||
const allowed = can('tasks');
|
||||
|
||||
const { data: tasks = [] } = useQuery<TaskSummary[]>({
|
||||
queryKey: ['tasks'],
|
||||
enabled: allowed,
|
||||
queryFn: () => client.get('/tasks'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const { data: categoryOrder = [] } = useQuery<string[]>({
|
||||
queryKey: ['task-categories'],
|
||||
enabled: allowed,
|
||||
queryFn: () => client.get('/tasks/categories'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ export { useAccessPolicy } from './useAccessPolicy';
|
||||
export { useClaudeSessions, useChatPwds } from './useClaudeSessions';
|
||||
export type { ClaudeSessionSummary, ClaudePwd } from './useClaudeSessions';
|
||||
export { useRecentModels } from './useRecentModels';
|
||||
export { usePlans } from './usePlans';
|
||||
export { useLandingPage } from './useLandingPage';
|
||||
export { useServerSettings } from './useServerSettings';
|
||||
export { useServerEnvironment } from './useServerEnvironment';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useCapabilities } from 'hooks/useCapabilities';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export const usePlans = () => {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { can } = useCapabilities();
|
||||
|
||||
const { data: plans = [] } = useQuery<string[]>({
|
||||
queryKey: ['PLANS'],
|
||||
enabled: isAuthenticated && can('plans'),
|
||||
queryFn: () => client.get<string[]>('/plans'),
|
||||
});
|
||||
|
||||
const getPlan = (name: string) => client.getText(`/plans/${name}`);
|
||||
|
||||
return { plans, getPlan };
|
||||
};
|
||||
Reference in New Issue
Block a user