@nexus-ai-fs/tui 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- package/src/stores/zones-store.ts +378 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace config editor: view and edit delegation namespace configuration.
|
|
3
|
+
*
|
|
4
|
+
* Fetches namespace detail from the backend endpoint and displays
|
|
5
|
+
* the delegation mode, scope constraints, grant modifications, and
|
|
6
|
+
* current mount table (visible paths).
|
|
7
|
+
*
|
|
8
|
+
* Press 'e' to enter edit mode, which allows editing scope_prefix,
|
|
9
|
+
* adding/removing grants, and adding/removing readonly paths.
|
|
10
|
+
* Tab cycles between editable fields, Enter saves, Escape cancels.
|
|
11
|
+
* Ctrl+D removes the last item from the focused list.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
15
|
+
import { useAccessStore } from "../../stores/access-store.js";
|
|
16
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
17
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
18
|
+
import { statusColor } from "../../shared/theme.js";
|
|
19
|
+
import type { FetchClient } from "@nexus-ai-fs/api-client";
|
|
20
|
+
|
|
21
|
+
type EditField = "scopePrefix" | "addGrant" | "removeGrant" | "readonlyPath";
|
|
22
|
+
|
|
23
|
+
const EDIT_FIELD_ORDER: readonly EditField[] = [
|
|
24
|
+
"scopePrefix",
|
|
25
|
+
"addGrant",
|
|
26
|
+
"removeGrant",
|
|
27
|
+
"readonlyPath",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
interface NamespaceConfigViewProps {
|
|
31
|
+
readonly delegationId: string;
|
|
32
|
+
readonly onClose: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function NamespaceConfigView({
|
|
36
|
+
delegationId,
|
|
37
|
+
onClose,
|
|
38
|
+
}: NamespaceConfigViewProps): React.ReactNode {
|
|
39
|
+
const client = useApi();
|
|
40
|
+
const namespaceDetail = useAccessStore((s) => s.namespaceDetail);
|
|
41
|
+
const namespaceDetailLoading = useAccessStore((s) => s.namespaceDetailLoading);
|
|
42
|
+
const error = useAccessStore((s) => s.error);
|
|
43
|
+
const fetchNamespaceDetail = useAccessStore((s) => s.fetchNamespaceDetail);
|
|
44
|
+
const updateNamespaceConfig = useAccessStore((s) => s.updateNamespaceConfig);
|
|
45
|
+
|
|
46
|
+
const [editing, setEditing] = useState(false);
|
|
47
|
+
const [activeField, setActiveField] = useState<EditField>("scopePrefix");
|
|
48
|
+
const [scopePrefix, setScopePrefix] = useState("");
|
|
49
|
+
const [addGrant, setAddGrant] = useState("");
|
|
50
|
+
const [removeGrant, setRemoveGrant] = useState("");
|
|
51
|
+
const [readonlyPath, setReadonlyPath] = useState("");
|
|
52
|
+
|
|
53
|
+
// Editable local copies of list arrays (populated on edit enter)
|
|
54
|
+
const [editRemovedGrants, setEditRemovedGrants] = useState<readonly string[]>([]);
|
|
55
|
+
const [editAddedGrants, setEditAddedGrants] = useState<readonly string[]>([]);
|
|
56
|
+
const [editReadonlyPaths, setEditReadonlyPaths] = useState<readonly string[]>([]);
|
|
57
|
+
|
|
58
|
+
// Resolved access: actual files + manifest permissions for this agent
|
|
59
|
+
interface ResolvedFile {
|
|
60
|
+
path: string;
|
|
61
|
+
isDirectory: boolean;
|
|
62
|
+
canRead: boolean;
|
|
63
|
+
canWrite: boolean;
|
|
64
|
+
blocked: boolean;
|
|
65
|
+
}
|
|
66
|
+
const [resolvedFiles, setResolvedFiles] = useState<readonly ResolvedFile[]>([]);
|
|
67
|
+
const [manifestEntries, setManifestEntries] = useState<readonly { tool_pattern: string; permission: string }[]>([]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (client && delegationId) {
|
|
71
|
+
fetchNamespaceDetail(delegationId, client);
|
|
72
|
+
}
|
|
73
|
+
}, [client, delegationId, fetchNamespaceDetail]);
|
|
74
|
+
|
|
75
|
+
// Fetch resolved files and manifest when namespace loads
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!client || !namespaceDetail) return;
|
|
78
|
+
const agentId = namespaceDetail.agent_id;
|
|
79
|
+
const scopePath = namespaceDetail.scope_prefix || "/";
|
|
80
|
+
const removedGrants = namespaceDetail.removed_grants ?? [];
|
|
81
|
+
|
|
82
|
+
// Fetch files under scope
|
|
83
|
+
const filesPromise = (client as FetchClient).get<{
|
|
84
|
+
items: readonly { path: string; isDirectory: boolean; name: string }[];
|
|
85
|
+
}>(`/api/v2/files/list?path=${encodeURIComponent(scopePath)}`).catch(() => ({ items: [] as any[] }));
|
|
86
|
+
|
|
87
|
+
// Fetch manifest list for this agent, then fetch detail to get entries
|
|
88
|
+
const manifestPromise = (client as FetchClient).get<{
|
|
89
|
+
manifests: readonly { manifest_id: string }[];
|
|
90
|
+
}>(`/api/v2/access-manifests?agent_id=${encodeURIComponent(agentId)}`)
|
|
91
|
+
.then((listResp) => {
|
|
92
|
+
const firstId = listResp.manifests?.[0]?.manifest_id;
|
|
93
|
+
if (!firstId) return { entries: [] as any[] };
|
|
94
|
+
return (client as FetchClient).get<{
|
|
95
|
+
entries: readonly { tool_pattern: string; permission: string }[];
|
|
96
|
+
}>(`/api/v2/access-manifests/${encodeURIComponent(firstId)}`);
|
|
97
|
+
})
|
|
98
|
+
.catch(() => ({ entries: [] as any[] }));
|
|
99
|
+
|
|
100
|
+
Promise.all([filesPromise, manifestPromise]).then(([filesResp, manifestResp]) => {
|
|
101
|
+
// Determine read/write from manifest entries
|
|
102
|
+
const entries = manifestResp.entries ?? [];
|
|
103
|
+
setManifestEntries(entries);
|
|
104
|
+
|
|
105
|
+
const hasPermission = (tool: string): boolean =>
|
|
106
|
+
entries.some((e) => {
|
|
107
|
+
const pattern = e.tool_pattern.replace(/\*/g, ".*");
|
|
108
|
+
return new RegExp(`^${pattern}$`).test(tool) && e.permission === "allow";
|
|
109
|
+
});
|
|
110
|
+
const isDenied = (tool: string): boolean =>
|
|
111
|
+
entries.some((e) => {
|
|
112
|
+
const pattern = e.tool_pattern.replace(/\*/g, ".*");
|
|
113
|
+
return new RegExp(`^${pattern}$`).test(tool) && e.permission === "deny";
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const canRead = hasPermission("file.read") || hasPermission("file.list");
|
|
117
|
+
const canWrite = hasPermission("file.write") && !isDenied("file.write");
|
|
118
|
+
|
|
119
|
+
const resolved: ResolvedFile[] = (filesResp.items ?? []).map((f) => {
|
|
120
|
+
const isBlocked = removedGrants.some((g) => {
|
|
121
|
+
const gPattern = g.replace(/\*/g, ".*");
|
|
122
|
+
return new RegExp(`^${gPattern}`).test(f.path);
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
path: f.path,
|
|
126
|
+
isDirectory: f.isDirectory,
|
|
127
|
+
canRead: !isBlocked && canRead,
|
|
128
|
+
canWrite: !isBlocked && canWrite,
|
|
129
|
+
blocked: isBlocked,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
setResolvedFiles(resolved);
|
|
133
|
+
});
|
|
134
|
+
}, [client, namespaceDetail]);
|
|
135
|
+
|
|
136
|
+
// Populate edit fields from fetched data
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (namespaceDetail) {
|
|
139
|
+
setScopePrefix(namespaceDetail.scope_prefix ?? "");
|
|
140
|
+
}
|
|
141
|
+
}, [namespaceDetail]);
|
|
142
|
+
|
|
143
|
+
const enterEditMode = useCallback(() => {
|
|
144
|
+
if (!namespaceDetail) return;
|
|
145
|
+
setEditing(true);
|
|
146
|
+
setActiveField("scopePrefix");
|
|
147
|
+
setScopePrefix(namespaceDetail.scope_prefix ?? "");
|
|
148
|
+
setEditRemovedGrants([...namespaceDetail.removed_grants]);
|
|
149
|
+
setEditAddedGrants([...namespaceDetail.added_grants]);
|
|
150
|
+
setEditReadonlyPaths([...namespaceDetail.readonly_paths]);
|
|
151
|
+
setAddGrant("");
|
|
152
|
+
setRemoveGrant("");
|
|
153
|
+
setReadonlyPath("");
|
|
154
|
+
}, [namespaceDetail]);
|
|
155
|
+
|
|
156
|
+
const setters: Readonly<Record<EditField, (fn: (b: string) => string) => void>> = {
|
|
157
|
+
scopePrefix: (fn) => setScopePrefix((b) => fn(b)),
|
|
158
|
+
addGrant: (fn) => setAddGrant((b) => fn(b)),
|
|
159
|
+
removeGrant: (fn) => setRemoveGrant((b) => fn(b)),
|
|
160
|
+
readonlyPath: (fn) => setReadonlyPath((b) => fn(b)),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleDeleteFromList = useCallback(() => {
|
|
164
|
+
if (!editing) return;
|
|
165
|
+
if (activeField === "removeGrant") {
|
|
166
|
+
setEditRemovedGrants((prev) => prev.slice(0, -1));
|
|
167
|
+
} else if (activeField === "addGrant") {
|
|
168
|
+
setEditAddedGrants((prev) => prev.slice(0, -1));
|
|
169
|
+
} else if (activeField === "readonlyPath") {
|
|
170
|
+
setEditReadonlyPaths((prev) => prev.slice(0, -1));
|
|
171
|
+
}
|
|
172
|
+
}, [editing, activeField]);
|
|
173
|
+
|
|
174
|
+
const handleSave = useCallback(() => {
|
|
175
|
+
if (!client || !namespaceDetail) return;
|
|
176
|
+
|
|
177
|
+
const update: {
|
|
178
|
+
scope_prefix?: string;
|
|
179
|
+
add_grants?: readonly string[];
|
|
180
|
+
remove_grants?: readonly string[];
|
|
181
|
+
readonly_paths?: readonly string[];
|
|
182
|
+
} = {};
|
|
183
|
+
|
|
184
|
+
// scope_prefix: send "" to clear, non-empty to set, omit to leave unchanged
|
|
185
|
+
const newPrefix = scopePrefix.trim();
|
|
186
|
+
const oldPrefix = namespaceDetail.scope_prefix ?? "";
|
|
187
|
+
if (newPrefix !== oldPrefix) {
|
|
188
|
+
update.scope_prefix = newPrefix;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build final arrays: local editable copy + any new text input
|
|
192
|
+
const finalRemoved = removeGrant.trim()
|
|
193
|
+
? [...editRemovedGrants, removeGrant.trim()]
|
|
194
|
+
: [...editRemovedGrants];
|
|
195
|
+
const finalAdded = addGrant.trim()
|
|
196
|
+
? [...editAddedGrants, addGrant.trim()]
|
|
197
|
+
: [...editAddedGrants];
|
|
198
|
+
const finalReadonly = readonlyPath.trim()
|
|
199
|
+
? [...editReadonlyPaths, readonlyPath.trim()]
|
|
200
|
+
: [...editReadonlyPaths];
|
|
201
|
+
|
|
202
|
+
// Send full replacement arrays if they differ from server state
|
|
203
|
+
if (JSON.stringify(finalRemoved) !== JSON.stringify(namespaceDetail.removed_grants)) {
|
|
204
|
+
update.remove_grants = finalRemoved;
|
|
205
|
+
}
|
|
206
|
+
if (JSON.stringify(finalAdded) !== JSON.stringify(namespaceDetail.added_grants)) {
|
|
207
|
+
update.add_grants = finalAdded;
|
|
208
|
+
}
|
|
209
|
+
if (JSON.stringify(finalReadonly) !== JSON.stringify(namespaceDetail.readonly_paths)) {
|
|
210
|
+
update.readonly_paths = finalReadonly;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (Object.keys(update).length > 0) {
|
|
214
|
+
updateNamespaceConfig(delegationId, update, client);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setAddGrant("");
|
|
218
|
+
setRemoveGrant("");
|
|
219
|
+
setReadonlyPath("");
|
|
220
|
+
setEditing(false);
|
|
221
|
+
}, [
|
|
222
|
+
client, delegationId, namespaceDetail, scopePrefix,
|
|
223
|
+
addGrant, removeGrant, readonlyPath,
|
|
224
|
+
editRemovedGrants, editAddedGrants, editReadonlyPaths,
|
|
225
|
+
updateNamespaceConfig,
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const handleUnhandledKey = useCallback(
|
|
229
|
+
(keyName: string) => {
|
|
230
|
+
if (!editing) return;
|
|
231
|
+
const setter = setters[activeField];
|
|
232
|
+
if (keyName.length === 1) {
|
|
233
|
+
setter((b) => b + keyName);
|
|
234
|
+
} else if (keyName === "space") {
|
|
235
|
+
setter((b) => b + " ");
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
[editing, activeField],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
useKeyboard(
|
|
242
|
+
{
|
|
243
|
+
escape: () => {
|
|
244
|
+
if (editing) {
|
|
245
|
+
setEditing(false);
|
|
246
|
+
} else {
|
|
247
|
+
onClose();
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
e: () => {
|
|
251
|
+
if (!editing && namespaceDetail) {
|
|
252
|
+
enterEditMode();
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
return: () => {
|
|
256
|
+
if (editing) {
|
|
257
|
+
handleSave();
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
backspace: () => {
|
|
261
|
+
if (editing) {
|
|
262
|
+
setters[activeField]((b) => b.slice(0, -1));
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
tab: () => {
|
|
266
|
+
if (editing) {
|
|
267
|
+
const currentIdx = EDIT_FIELD_ORDER.indexOf(activeField);
|
|
268
|
+
const nextIdx = (currentIdx + 1) % EDIT_FIELD_ORDER.length;
|
|
269
|
+
const next = EDIT_FIELD_ORDER[nextIdx];
|
|
270
|
+
if (next) {
|
|
271
|
+
setActiveField(next);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"ctrl+d": () => {
|
|
276
|
+
handleDeleteFromList();
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
handleUnhandledKey,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const ns = namespaceDetail;
|
|
283
|
+
const cursor = "\u2588";
|
|
284
|
+
|
|
285
|
+
// In edit mode, show local editable arrays; in view mode, show server data
|
|
286
|
+
const displayRemovedGrants = editing ? editRemovedGrants : (ns?.removed_grants ?? []);
|
|
287
|
+
const displayAddedGrants = editing ? editAddedGrants : (ns?.added_grants ?? []);
|
|
288
|
+
const displayReadonlyPaths = editing ? editReadonlyPaths : (ns?.readonly_paths ?? []);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
292
|
+
<box height={1} width="100%">
|
|
293
|
+
<text>{`--- Namespace Config${editing ? " [EDITING]" : ""}: ${delegationId} ---`}</text>
|
|
294
|
+
</box>
|
|
295
|
+
|
|
296
|
+
{namespaceDetailLoading && (
|
|
297
|
+
<box height={1} width="100%">
|
|
298
|
+
<text>Loading namespace details...</text>
|
|
299
|
+
</box>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{error && !namespaceDetailLoading && (
|
|
303
|
+
<box height={1} width="100%">
|
|
304
|
+
<text>{`Error: ${error}`}</text>
|
|
305
|
+
</box>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{ns && !namespaceDetailLoading && (
|
|
309
|
+
<>
|
|
310
|
+
{/* Plain-English summary */}
|
|
311
|
+
<box height={1} width="100%">
|
|
312
|
+
<text bold foregroundColor={statusColor.info}>{" What can this agent access?"}</text>
|
|
313
|
+
</box>
|
|
314
|
+
<box height={1} width="100%">
|
|
315
|
+
<text>
|
|
316
|
+
{ns.delegation_mode === "clean"
|
|
317
|
+
? ns.scope_prefix
|
|
318
|
+
? ` ✓ Files under ${ns.scope_prefix}/* only (clean namespace)`
|
|
319
|
+
: " ✓ Empty namespace — no inherited files (clean mode)"
|
|
320
|
+
: ns.delegation_mode === "copy"
|
|
321
|
+
? " ✓ Independent copy of parent's files (changes don't sync)"
|
|
322
|
+
: " ✓ Same files as parent agent (shared, changes sync both ways)"}
|
|
323
|
+
</text>
|
|
324
|
+
</box>
|
|
325
|
+
{displayRemovedGrants.length > 0 && (
|
|
326
|
+
<box height={1} width="100%">
|
|
327
|
+
<text foregroundColor={statusColor.error}>{` ✗ BLOCKED: ${displayRemovedGrants.join(", ")}`}</text>
|
|
328
|
+
</box>
|
|
329
|
+
)}
|
|
330
|
+
{displayReadonlyPaths.length > 0 && (
|
|
331
|
+
<box height={1} width="100%">
|
|
332
|
+
<text foregroundColor={statusColor.warning}>{` ◐ READ-ONLY: ${displayReadonlyPaths.join(", ")}`}</text>
|
|
333
|
+
</box>
|
|
334
|
+
)}
|
|
335
|
+
{/* Tool permissions from manifest */}
|
|
336
|
+
{manifestEntries.length > 0 && (
|
|
337
|
+
<>
|
|
338
|
+
<box height={1} width="100%"><text>{""}</text></box>
|
|
339
|
+
<box height={1} width="100%">
|
|
340
|
+
<text bold foregroundColor={statusColor.info}>{" Tool permissions (from manifest):"}</text>
|
|
341
|
+
</box>
|
|
342
|
+
{manifestEntries.map((e, i) => (
|
|
343
|
+
<box key={`me-${i}`} height={1} width="100%">
|
|
344
|
+
<text>
|
|
345
|
+
<span foregroundColor={e.permission === "allow" ? statusColor.success : statusColor.error}>
|
|
346
|
+
{e.permission === "allow" ? " ✓ " : " ✗ "}
|
|
347
|
+
</span>
|
|
348
|
+
<span>{`${e.tool_pattern}`}</span>
|
|
349
|
+
<span dimColor>{` (${e.permission})`}</span>
|
|
350
|
+
</text>
|
|
351
|
+
</box>
|
|
352
|
+
))}
|
|
353
|
+
</>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{/* Scope access summary */}
|
|
357
|
+
<box height={1} width="100%"><text>{""}</text></box>
|
|
358
|
+
<box height={1} width="100%">
|
|
359
|
+
<text bold foregroundColor={statusColor.info}>{" File access scope:"}</text>
|
|
360
|
+
</box>
|
|
361
|
+
{(() => {
|
|
362
|
+
const scope = ns.scope_prefix || "(all paths)";
|
|
363
|
+
const canRead = manifestEntries.some((e) => e.permission === "allow" && /^file\.\*$|^file\.read$|^file\.list$/.test(e.tool_pattern));
|
|
364
|
+
const canWrite = manifestEntries.some((e) => e.permission === "allow" && /^file\.\*$|^file\.write$/.test(e.tool_pattern));
|
|
365
|
+
const writeDenied = manifestEntries.some((e) => e.permission === "deny" && /^file\.\*$|^file\.write$/.test(e.tool_pattern));
|
|
366
|
+
const effectiveWrite = canWrite && !writeDenied;
|
|
367
|
+
const badge = effectiveWrite ? "[RW]" : canRead ? "[R-]" : "[--]";
|
|
368
|
+
const badgeColor = effectiveWrite ? statusColor.success : canRead ? statusColor.info : statusColor.dim;
|
|
369
|
+
const fileCount = resolvedFiles.filter((f) => !f.blocked).length;
|
|
370
|
+
const dirCount = resolvedFiles.filter((f) => f.isDirectory && !f.blocked).length;
|
|
371
|
+
const blockedCount = resolvedFiles.filter((f) => f.blocked).length;
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<>
|
|
375
|
+
<box height={1} width="100%">
|
|
376
|
+
<text>
|
|
377
|
+
<span foregroundColor={badgeColor}>{` ${badge} `}</span>
|
|
378
|
+
<span>{scope === "(all paths)" ? scope : `${scope}/*`}</span>
|
|
379
|
+
<span dimColor>{fileCount > 0 ? ` (${fileCount} files${dirCount > 0 ? `, ${dirCount} dirs` : ""})` : ""}</span>
|
|
380
|
+
</text>
|
|
381
|
+
</box>
|
|
382
|
+
{displayRemovedGrants.map((g, i) => (
|
|
383
|
+
<box key={`blocked-${i}`} height={1} width="100%">
|
|
384
|
+
<text>
|
|
385
|
+
<span foregroundColor={statusColor.error}>{" [--] "}</span>
|
|
386
|
+
<span>{`${g}`}</span>
|
|
387
|
+
<span foregroundColor={statusColor.error}>{" BLOCKED"}</span>
|
|
388
|
+
</text>
|
|
389
|
+
</box>
|
|
390
|
+
))}
|
|
391
|
+
{displayReadonlyPaths.map((p, i) => (
|
|
392
|
+
<box key={`ro-scope-${i}`} height={1} width="100%">
|
|
393
|
+
<text>
|
|
394
|
+
<span foregroundColor={statusColor.warning}>{" [R-] "}</span>
|
|
395
|
+
<span>{`${p}`}</span>
|
|
396
|
+
<span foregroundColor={statusColor.warning}>{" READ-ONLY"}</span>
|
|
397
|
+
</text>
|
|
398
|
+
</box>
|
|
399
|
+
))}
|
|
400
|
+
{blockedCount > 0 && (
|
|
401
|
+
<box height={1} width="100%">
|
|
402
|
+
<text dimColor>{` ${blockedCount} path(s) blocked by removed grants`}</text>
|
|
403
|
+
</box>
|
|
404
|
+
)}
|
|
405
|
+
</>
|
|
406
|
+
);
|
|
407
|
+
})()}
|
|
408
|
+
|
|
409
|
+
<box height={1} width="100%"><text>{""}</text></box>
|
|
410
|
+
|
|
411
|
+
{/* Technical details */}
|
|
412
|
+
<box height={1} width="100%">
|
|
413
|
+
<text dimColor>{" ─── Technical Details ───"}</text>
|
|
414
|
+
</box>
|
|
415
|
+
<box height={1} width="100%">
|
|
416
|
+
<text>{` Agent: ${ns.agent_id}`}</text>
|
|
417
|
+
</box>
|
|
418
|
+
<box height={1} width="100%">
|
|
419
|
+
<text>{` Mode: ${ns.delegation_mode} (immutable)`}</text>
|
|
420
|
+
</box>
|
|
421
|
+
|
|
422
|
+
{/* Scope prefix — editable */}
|
|
423
|
+
{editing ? (
|
|
424
|
+
<box height={1} width="100%">
|
|
425
|
+
<text>
|
|
426
|
+
{activeField === "scopePrefix"
|
|
427
|
+
? `> Prefix: ${scopePrefix}${cursor}`
|
|
428
|
+
: ` Prefix: ${scopePrefix}`}
|
|
429
|
+
</text>
|
|
430
|
+
</box>
|
|
431
|
+
) : (
|
|
432
|
+
<box height={1} width="100%">
|
|
433
|
+
<text>{` Prefix: ${ns.scope_prefix ?? "(none)"}`}</text>
|
|
434
|
+
</box>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
<box height={1} width="100%">
|
|
438
|
+
<text>{` Zone: ${ns.zone_id ?? "(none)"}`}</text>
|
|
439
|
+
</box>
|
|
440
|
+
|
|
441
|
+
{/* Removed grants */}
|
|
442
|
+
<box height={1} width="100%">
|
|
443
|
+
<text>{` Removed grants (${displayRemovedGrants.length}):`}</text>
|
|
444
|
+
</box>
|
|
445
|
+
{displayRemovedGrants.map((g, i) => (
|
|
446
|
+
<box key={`rg-${i}`} height={1} width="100%">
|
|
447
|
+
<text>{` - ${g}`}</text>
|
|
448
|
+
</box>
|
|
449
|
+
))}
|
|
450
|
+
{editing && (
|
|
451
|
+
<box height={1} width="100%">
|
|
452
|
+
<text>
|
|
453
|
+
{activeField === "removeGrant"
|
|
454
|
+
? `> + remove: ${removeGrant}${cursor}`
|
|
455
|
+
: ` + remove: ${removeGrant}`}
|
|
456
|
+
</text>
|
|
457
|
+
</box>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{/* Added grants */}
|
|
461
|
+
<box height={1} width="100%">
|
|
462
|
+
<text>{` Added grants (${displayAddedGrants.length}):`}</text>
|
|
463
|
+
</box>
|
|
464
|
+
{displayAddedGrants.map((g, i) => (
|
|
465
|
+
<box key={`ag-${i}`} height={1} width="100%">
|
|
466
|
+
<text>{` + ${g}`}</text>
|
|
467
|
+
</box>
|
|
468
|
+
))}
|
|
469
|
+
{editing && (
|
|
470
|
+
<box height={1} width="100%">
|
|
471
|
+
<text>
|
|
472
|
+
{activeField === "addGrant"
|
|
473
|
+
? `> + add: ${addGrant}${cursor}`
|
|
474
|
+
: ` + add: ${addGrant}`}
|
|
475
|
+
</text>
|
|
476
|
+
</box>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{/* Readonly paths */}
|
|
480
|
+
<box height={1} width="100%">
|
|
481
|
+
<text>{` Read-only paths (${displayReadonlyPaths.length}):`}</text>
|
|
482
|
+
</box>
|
|
483
|
+
{displayReadonlyPaths.map((p, i) => (
|
|
484
|
+
<box key={`ro-${i}`} height={1} width="100%">
|
|
485
|
+
<text>{` [RO] ${p}`}</text>
|
|
486
|
+
</box>
|
|
487
|
+
))}
|
|
488
|
+
{editing && (
|
|
489
|
+
<box height={1} width="100%">
|
|
490
|
+
<text>
|
|
491
|
+
{activeField === "readonlyPath"
|
|
492
|
+
? `> + readonly: ${readonlyPath}${cursor}`
|
|
493
|
+
: ` + readonly: ${readonlyPath}`}
|
|
494
|
+
</text>
|
|
495
|
+
</box>
|
|
496
|
+
)}
|
|
497
|
+
|
|
498
|
+
{/* Mount table */}
|
|
499
|
+
<box height={1} width="100%">
|
|
500
|
+
<text>{` Mount table (${ns.mount_table.length} entries):`}</text>
|
|
501
|
+
</box>
|
|
502
|
+
{ns.mount_table.length > 0 ? (
|
|
503
|
+
ns.mount_table.map((path, i) => (
|
|
504
|
+
<box key={`mt-${i}`} height={1} width="100%">
|
|
505
|
+
<text>{` ${path}`}</text>
|
|
506
|
+
</box>
|
|
507
|
+
))
|
|
508
|
+
) : (
|
|
509
|
+
<box height={1} width="100%">
|
|
510
|
+
<text>{" (empty)"}</text>
|
|
511
|
+
</box>
|
|
512
|
+
)}
|
|
513
|
+
</>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
<box height={1} width="100%">
|
|
517
|
+
<text>
|
|
518
|
+
{editing
|
|
519
|
+
? "Tab:next field Enter:save Escape:cancel Backspace:delete char Ctrl+D:remove last item"
|
|
520
|
+
: "e:edit Escape:close"}
|
|
521
|
+
</text>
|
|
522
|
+
</box>
|
|
523
|
+
</box>
|
|
524
|
+
);
|
|
525
|
+
}
|