@celestea/studio 2.7.1
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/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- package/webdist/index.html +261 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine request -> host view (W725): the projection behind
|
|
3
|
+
* `GET /api/sessions/{id}/context`.
|
|
4
|
+
*
|
|
5
|
+
* The projection is deliberately thin and lossless in the direction that
|
|
6
|
+
* matters: everything the model sees comes from the agent loop's own
|
|
7
|
+
* `contextSnapshot()` (system prompt, post-trim derived history, tool schemas).
|
|
8
|
+
* This module never trims, filters or re-derives a single message — it only
|
|
9
|
+
* flattens the engine's `Message[]` into one display row per message, because
|
|
10
|
+
* the wire contract carries `content` as a string.
|
|
11
|
+
*
|
|
12
|
+
* Flattening rules (frozen; the frontend renders them verbatim):
|
|
13
|
+
* - a message's content blocks are joined with "\n";
|
|
14
|
+
* - a `tool_call` block contributes `[tool_call] <name> <compact JSON args>`;
|
|
15
|
+
* - an assistant tool-call row carries the FIRST call's `tool_name` +
|
|
16
|
+
* `tool_call_id` (`tool_name` = what it called, id = the call it made);
|
|
17
|
+
* - a `tool` result row carries its own `tool_call_id`, resolved back to the
|
|
18
|
+
* call's `tool_name` while the messages are walked in order.
|
|
19
|
+
*/
|
|
20
|
+
/** One message's content blocks as a single display string. */
|
|
21
|
+
export function renderContent(content) {
|
|
22
|
+
return content
|
|
23
|
+
.map((block) => {
|
|
24
|
+
// W804: an image block is NOT a tool call; render it as a byte-free label.
|
|
25
|
+
if (block.type === "text")
|
|
26
|
+
return block.content;
|
|
27
|
+
if (block.type === "image")
|
|
28
|
+
return imageText(block.content);
|
|
29
|
+
return callText(block.content);
|
|
30
|
+
})
|
|
31
|
+
.join("\n");
|
|
32
|
+
}
|
|
33
|
+
/** W804: e.g. `[image] logo.png image/png 512x512 (attachment <sha>)`. */
|
|
34
|
+
export function imageText(ref) {
|
|
35
|
+
const label = ref.name === undefined ? "" : ` ${ref.name}`;
|
|
36
|
+
return `[image]${label} ${ref.media_type} ${ref.width}x${ref.height} (attachment ${ref.attachment_id})`;
|
|
37
|
+
}
|
|
38
|
+
/** `Message[]` -> one view row each, resolving tool names in message order. */
|
|
39
|
+
export function messageViews(messages) {
|
|
40
|
+
const names = new Map();
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (const message of messages) {
|
|
43
|
+
const call = firstCall(message);
|
|
44
|
+
if (call !== null)
|
|
45
|
+
names.set(call.id, call.name);
|
|
46
|
+
rows.push(rowOf(message, names));
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The session's context as the engine assembled it. `runtime.contextSnapshot()`
|
|
52
|
+
* is the ONLY source; the fallback (a loop without the capability) reports the
|
|
53
|
+
* profile's system prompt and the composed tool schemas with no history.
|
|
54
|
+
*/
|
|
55
|
+
export function contextViewOf(runtime, fallback) {
|
|
56
|
+
const request = runtime.contextSnapshot();
|
|
57
|
+
if (request === null) {
|
|
58
|
+
return { model: fallback.model, system: fallback.system, tools: toolViews(fallback.tools), messages: [] };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
model: request.model,
|
|
62
|
+
system: request.system ?? "",
|
|
63
|
+
tools: toolViews(request.tools),
|
|
64
|
+
messages: messageViews(request.messages),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* W769: the read-only view helpers the adapter's HTTP surface needs, so the
|
|
69
|
+
* adapter stays a router instead of an assembly site (it is at its size budget).
|
|
70
|
+
*
|
|
71
|
+
* `sessionContextOf` is `/api/sessions/{id}/context` for one already-composed
|
|
72
|
+
* runtime: the read-only view of a runtime the caller already resolved.
|
|
73
|
+
*/
|
|
74
|
+
export function sessionContextOf(runtime, profile) {
|
|
75
|
+
return contextViewOf(runtime, { model: profile.model, system: profile.system_prompt, tools: runtime.tools?.schemas() ?? [] });
|
|
76
|
+
}
|
|
77
|
+
/** Tool schemas pass through verbatim (name / description / parameters). */
|
|
78
|
+
export function toolViews(specs) {
|
|
79
|
+
return specs.map((spec) => ({ name: spec.name, description: spec.description, parameters: spec.parameters }));
|
|
80
|
+
}
|
|
81
|
+
function rowOf(message, names) {
|
|
82
|
+
const row = { role: message.role, content: renderContent(message.content) };
|
|
83
|
+
const call = firstCall(message);
|
|
84
|
+
if (call !== null) {
|
|
85
|
+
row.tool_name = call.name;
|
|
86
|
+
row.tool_call_id = call.id;
|
|
87
|
+
}
|
|
88
|
+
const answered = message.tool_call_id;
|
|
89
|
+
if (answered !== null) {
|
|
90
|
+
row.tool_call_id = answered;
|
|
91
|
+
const name = names.get(answered);
|
|
92
|
+
if (name !== undefined)
|
|
93
|
+
row.tool_name = name;
|
|
94
|
+
}
|
|
95
|
+
return row;
|
|
96
|
+
}
|
|
97
|
+
function firstCall(message) {
|
|
98
|
+
for (const block of message.content)
|
|
99
|
+
if (block.type === "tool_call")
|
|
100
|
+
return block.content;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function callText(call) {
|
|
104
|
+
return `[tool_call] ${call.name} ${argsText(call.args)}`;
|
|
105
|
+
}
|
|
106
|
+
function argsText(args) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(args) ?? "null";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return "<unserializable args>";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `effectiveGrantsOf` — the ONE reader of a session's grants (W516 §4.1/§4.3).
|
|
3
|
+
*
|
|
4
|
+
* Every rule here is fail-closed and "only widen":
|
|
5
|
+
* 1. file level: missing = no grants; bad JSON / unknown version / a
|
|
6
|
+
* self-description that does not match the directory / `grants` not an
|
|
7
|
+
* array = the WHOLE file is void + a warning (never repaired, never
|
|
8
|
+
* guessed at);
|
|
9
|
+
* 2. entry level: an unknown `cap` or a wrong-typed `scope` drops THAT entry;
|
|
10
|
+
* 3. path scopes must be absolute, `realpath`-able directories, not `/`, not
|
|
11
|
+
* the data dir, not `$HOME`, and a `write_roots` entry must not overlap a
|
|
12
|
+
* read root (workspace/env/other grants) — all six must hold or the entry
|
|
13
|
+
* is ignored;
|
|
14
|
+
* 4. roots are additive: the workspace and the env roots are never removed;
|
|
15
|
+
* 5. `unsandboxed` is only ever *consumed* by the provider policy, which
|
|
16
|
+
* ignores it while bwrap works (`sandbox/provider.ts`) — this module only
|
|
17
|
+
* validates it;
|
|
18
|
+
* 6. unparseable `net_hosts` entries are ignored one by one;
|
|
19
|
+
* 7. ignoring a bad entry is deliberate and the OPPOSITE of the env
|
|
20
|
+
* `CELESTEA_TOOL_ROOTS` fail-closed rule: env is an operator posture where
|
|
21
|
+
* a typo must be loud, grants are per-session widenings where "ignore" is
|
|
22
|
+
* the safe side, while a hard failure would only push users toward
|
|
23
|
+
* `CELESTEA_TOOL_GUARD=0`. Do NOT "unify" the two.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* W747: `sessionIdOfDir` moved to the engine (`@celestea/runtime`, host layer
|
|
27
|
+
* `host/engine-session.ts`) — the `<workspace>/<session>` id space is what that
|
|
28
|
+
* module already owns. Imported + re-exported here so every `./engine-grants.js`
|
|
29
|
+
* import of it keeps working, unchanged.
|
|
30
|
+
*/
|
|
31
|
+
import { sessionIdOfDir } from "@celestea/runtime";
|
|
32
|
+
export { sessionIdOfDir };
|
|
33
|
+
import type { GrantsAuditEventName } from "../store/grants-audit.js";
|
|
34
|
+
import { type PermissionPreset } from "../store/permissions.js";
|
|
35
|
+
import { type GrantRecord, type GrantsFile } from "../store/grants.js";
|
|
36
|
+
/** The effective, widen-only grant set of ONE session instance. */
|
|
37
|
+
export interface EffectiveGrants {
|
|
38
|
+
network: boolean;
|
|
39
|
+
readRoots: readonly string[];
|
|
40
|
+
writeRoots: readonly string[];
|
|
41
|
+
netHosts: readonly string[];
|
|
42
|
+
toolExtra: readonly string[];
|
|
43
|
+
unsandboxed: boolean;
|
|
44
|
+
/** W9: the permission baseline of the session (enforced by the sandbox/guard). */
|
|
45
|
+
workspaceWritable: boolean;
|
|
46
|
+
/** W9: tools the baseline removes from the face (before tool_extra adds). */
|
|
47
|
+
toolDeny: readonly string[];
|
|
48
|
+
/** Provenance for the audit trail / UI (`cap` + grant id + expiry). */
|
|
49
|
+
sources: ReadonlyArray<{
|
|
50
|
+
cap: string;
|
|
51
|
+
grantId: string;
|
|
52
|
+
expiresAt: number | null;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export declare const EMPTY_GRANTS: EffectiveGrants;
|
|
56
|
+
export interface EffectiveGrantsResult {
|
|
57
|
+
grants: EffectiveGrants;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
/** `unsandboxed` is only offered when the operator opts in (§2.2). */
|
|
61
|
+
export declare function unsandboxedAvailable(env: NodeJS.ProcessEnv): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* W757: whether this session's `net_hosts` entries take effect AT ALL in this
|
|
64
|
+
* deployment.
|
|
65
|
+
*
|
|
66
|
+
* ONE definition, and deliberately the SAME construction path the engine mounts
|
|
67
|
+
* its tools with (`engineTools` → `httpOptions` → `HttpTargetPolicy.fromEnv`):
|
|
68
|
+
* `net_hosts` is merged into the allow side only, so an inactive env policy
|
|
69
|
+
* (neither `CELESTEA_HTTP_ALLOW` nor `CELESTEA_HTTP_DENY` set) drops the whole
|
|
70
|
+
* list — `netHostsIneffective` is exactly that verdict and this module never
|
|
71
|
+
* re-derives it (a second implementation would silently drift from the mount).
|
|
72
|
+
*
|
|
73
|
+
* `false` ⇒ the session holds `net_hosts` entries the deployment ignores
|
|
74
|
+
* entirely. Empty `net_hosts` ⇒ `true`: there is nothing to be dropped.
|
|
75
|
+
*
|
|
76
|
+
* Reporting only: the policy object is built and discarded, so no authorization
|
|
77
|
+
* decision here changes, and the union / deny-wins / fail-closed semantics of
|
|
78
|
+
* `ssrf.ts` stay exactly as they are.
|
|
79
|
+
*/
|
|
80
|
+
export declare function netHostsEffective(env: NodeJS.ProcessEnv, grants: EffectiveGrants): boolean;
|
|
81
|
+
/** Read + validate; NEVER throws, only degrades with warnings (§4.1). */
|
|
82
|
+
/**
|
|
83
|
+
* W9: read the session's permission baseline and intersect its grants into it
|
|
84
|
+
* (the permission is authoritative; grants only widen INSIDE it).
|
|
85
|
+
*/
|
|
86
|
+
export declare function effectiveGrantsOf(sessionDir: string | null, sessionId: string | null, env: NodeJS.ProcessEnv, now: number, presetHint?: string | null): EffectiveGrantsResult;
|
|
87
|
+
/** Env-declared read roots, canonicalized best-effort (grant overlap check). */
|
|
88
|
+
export declare function envReadRoots(env: NodeJS.ProcessEnv): string[];
|
|
89
|
+
/**
|
|
90
|
+
* What the assembly layers may report back (W516 §4.4). Deliberately tiny and
|
|
91
|
+
* credential-free: no command text ever travels through here.
|
|
92
|
+
*/
|
|
93
|
+
export interface EngineGrantEvent {
|
|
94
|
+
event: GrantsAuditEventName;
|
|
95
|
+
cap?: string;
|
|
96
|
+
grant_id?: string;
|
|
97
|
+
provider?: string;
|
|
98
|
+
reason?: string;
|
|
99
|
+
detail?: string;
|
|
100
|
+
}
|
|
101
|
+
/** Sink bound to one session (see `session-grants.ts`). */
|
|
102
|
+
export type EngineGrantAudit = (event: EngineGrantEvent) => void;
|
|
103
|
+
/** Cap names currently in force (never paths) — `GET /api/status` (§5.7). */
|
|
104
|
+
/**
|
|
105
|
+
* W9: validate a CUSTOM permission preset, reusing the grants root rules
|
|
106
|
+
* (`rejectRoot`) so a preset can never declare a root an equivalent
|
|
107
|
+
* `write_roots` grant would have been refused (data dir, $HOME, `/`,
|
|
108
|
+
* non-absolute, non-existent, credential-shaped, read-root overlap).
|
|
109
|
+
* Returns `conflict: true` for a built-in or already-taken id.
|
|
110
|
+
*/
|
|
111
|
+
export declare function validatePermissionPreset(raw: unknown, env: NodeJS.ProcessEnv, existingIds: readonly string[]): {
|
|
112
|
+
ok: true;
|
|
113
|
+
preset: PermissionPreset;
|
|
114
|
+
} | {
|
|
115
|
+
ok: false;
|
|
116
|
+
error: string;
|
|
117
|
+
conflict?: boolean;
|
|
118
|
+
};
|
|
119
|
+
export declare function grantsActiveCaps(grants: EffectiveGrants): string[];
|
|
120
|
+
/** Grant ids that are past their `expires_at` (the `expire` audit event). */
|
|
121
|
+
export declare function expiredGrants(file: GrantsFile, now: number): GrantRecord[];
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `effectiveGrantsOf` — the ONE reader of a session's grants (W516 §4.1/§4.3).
|
|
3
|
+
*
|
|
4
|
+
* Every rule here is fail-closed and "only widen":
|
|
5
|
+
* 1. file level: missing = no grants; bad JSON / unknown version / a
|
|
6
|
+
* self-description that does not match the directory / `grants` not an
|
|
7
|
+
* array = the WHOLE file is void + a warning (never repaired, never
|
|
8
|
+
* guessed at);
|
|
9
|
+
* 2. entry level: an unknown `cap` or a wrong-typed `scope` drops THAT entry;
|
|
10
|
+
* 3. path scopes must be absolute, `realpath`-able directories, not `/`, not
|
|
11
|
+
* the data dir, not `$HOME`, and a `write_roots` entry must not overlap a
|
|
12
|
+
* read root (workspace/env/other grants) — all six must hold or the entry
|
|
13
|
+
* is ignored;
|
|
14
|
+
* 4. roots are additive: the workspace and the env roots are never removed;
|
|
15
|
+
* 5. `unsandboxed` is only ever *consumed* by the provider policy, which
|
|
16
|
+
* ignores it while bwrap works (`sandbox/provider.ts`) — this module only
|
|
17
|
+
* validates it;
|
|
18
|
+
* 6. unparseable `net_hosts` entries are ignored one by one;
|
|
19
|
+
* 7. ignoring a bad entry is deliberate and the OPPOSITE of the env
|
|
20
|
+
* `CELESTEA_TOOL_ROOTS` fail-closed rule: env is an operator posture where
|
|
21
|
+
* a typo must be loud, grants are per-session widenings where "ignore" is
|
|
22
|
+
* the safe side, while a hard failure would only push users toward
|
|
23
|
+
* `CELESTEA_TOOL_GUARD=0`. Do NOT "unify" the two.
|
|
24
|
+
*/
|
|
25
|
+
import { realpathSync, statSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { dirname, isAbsolute } from "node:path";
|
|
28
|
+
import { httpOptions, isInside, parseIpRange, parseToolRoots } from "@celestea/tools";
|
|
29
|
+
/**
|
|
30
|
+
* W747: `sessionIdOfDir` moved to the engine (`@celestea/runtime`, host layer
|
|
31
|
+
* `host/engine-session.ts`) — the `<workspace>/<session>` id space is what that
|
|
32
|
+
* module already owns. Imported + re-exported here so every `./engine-grants.js`
|
|
33
|
+
* import of it keeps working, unchanged.
|
|
34
|
+
*/
|
|
35
|
+
import { sessionIdOfDir } from "@celestea/runtime";
|
|
36
|
+
export { sessionIdOfDir };
|
|
37
|
+
import { loadStudioConfig } from "../config.js";
|
|
38
|
+
import { isBuiltinPresetId, parsePreset } from "../store/permissions.js";
|
|
39
|
+
import { effectivePermissionOf } from "./engine-permissions.js";
|
|
40
|
+
import { readSessionTools } from "../store/session-tools.js";
|
|
41
|
+
import { ENV_GRANTS_UNSANDBOXED, isExpired, knownSecretsOf, looksLikeCredential, readGrantsFile, } from "../store/grants.js";
|
|
42
|
+
export const EMPTY_GRANTS = {
|
|
43
|
+
network: false,
|
|
44
|
+
readRoots: [],
|
|
45
|
+
writeRoots: [],
|
|
46
|
+
netHosts: [],
|
|
47
|
+
toolExtra: [],
|
|
48
|
+
unsandboxed: false,
|
|
49
|
+
workspaceWritable: true,
|
|
50
|
+
toolDeny: [],
|
|
51
|
+
sources: [],
|
|
52
|
+
};
|
|
53
|
+
/** `unsandboxed` is only offered when the operator opts in (§2.2). */
|
|
54
|
+
export function unsandboxedAvailable(env) {
|
|
55
|
+
const raw = (env[ENV_GRANTS_UNSANDBOXED] ?? "").trim().toLowerCase();
|
|
56
|
+
return ["1", "true", "on", "yes"].includes(raw);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* W757: whether this session's `net_hosts` entries take effect AT ALL in this
|
|
60
|
+
* deployment.
|
|
61
|
+
*
|
|
62
|
+
* ONE definition, and deliberately the SAME construction path the engine mounts
|
|
63
|
+
* its tools with (`engineTools` → `httpOptions` → `HttpTargetPolicy.fromEnv`):
|
|
64
|
+
* `net_hosts` is merged into the allow side only, so an inactive env policy
|
|
65
|
+
* (neither `CELESTEA_HTTP_ALLOW` nor `CELESTEA_HTTP_DENY` set) drops the whole
|
|
66
|
+
* list — `netHostsIneffective` is exactly that verdict and this module never
|
|
67
|
+
* re-derives it (a second implementation would silently drift from the mount).
|
|
68
|
+
*
|
|
69
|
+
* `false` ⇒ the session holds `net_hosts` entries the deployment ignores
|
|
70
|
+
* entirely. Empty `net_hosts` ⇒ `true`: there is nothing to be dropped.
|
|
71
|
+
*
|
|
72
|
+
* Reporting only: the policy object is built and discarded, so no authorization
|
|
73
|
+
* decision here changes, and the union / deny-wins / fail-closed semantics of
|
|
74
|
+
* `ssrf.ts` stay exactly as they are.
|
|
75
|
+
*/
|
|
76
|
+
export function netHostsEffective(env, grants) {
|
|
77
|
+
return httpOptions(env, { netHosts: grants.netHosts }).policy?.netHostsIneffective !== true;
|
|
78
|
+
}
|
|
79
|
+
/** Read + validate; NEVER throws, only degrades with warnings (§4.1). */
|
|
80
|
+
/**
|
|
81
|
+
* W9: read the session's permission baseline and intersect its grants into it
|
|
82
|
+
* (the permission is authoritative; grants only widen INSIDE it).
|
|
83
|
+
*/
|
|
84
|
+
export function effectiveGrantsOf(sessionDir, sessionId, env, now, presetHint) {
|
|
85
|
+
const permission = effectivePermissionOf(sessionDir, sessionId, env, presetHint);
|
|
86
|
+
/**
|
|
87
|
+
* W860: the session's own DISABLED tool list is read HERE, through the same
|
|
88
|
+
* reader every other consumer of the effective grants goes through
|
|
89
|
+
* (`session-grants.ts` for the composed instance, `RealRuntimeAdapter.sessionTools`
|
|
90
|
+
* for `GET /api/tools?session=`). A void `tools.json` warns and changes no
|
|
91
|
+
* other cap; a session without a directory has nothing to read, so its
|
|
92
|
+
* `toolDeny` is exactly the permission baseline's.
|
|
93
|
+
*/
|
|
94
|
+
// W878: both sidecars are self-describing, so the TRUSTED id from `resolve()`
|
|
95
|
+
// must be used; a null id with a real directory is a caller bug and degrades
|
|
96
|
+
// fail-closed (nothing readable) rather than falling back to path inference.
|
|
97
|
+
const sessionTools = sessionDir === null || sessionId === null ? { disabled: [], warnings: [] } : readSessionTools(sessionDir, sessionId);
|
|
98
|
+
const base = collectGrants(sessionDir, sessionId, env, now);
|
|
99
|
+
const merged = intersectGrants(base.grants, permission, env, sessionTools.disabled);
|
|
100
|
+
return { grants: merged.grants, warnings: [...permission.warnings, ...sessionTools.warnings, ...base.warnings, ...merged.warnings] };
|
|
101
|
+
}
|
|
102
|
+
function collectGrants(sessionDir, sessionId, env, now) {
|
|
103
|
+
if (sessionDir === null || sessionId === null)
|
|
104
|
+
return { grants: EMPTY_GRANTS, warnings: [] };
|
|
105
|
+
const read = readGrantsFile(sessionDir, sessionId);
|
|
106
|
+
if (!read.exists)
|
|
107
|
+
return { grants: EMPTY_GRANTS, warnings: [] };
|
|
108
|
+
if (read.file === undefined) {
|
|
109
|
+
const reason = read.error ?? "unreadable";
|
|
110
|
+
return { grants: EMPTY_GRANTS, warnings: [`grants_unreadable: ${reason} — the session runs with no grants`] };
|
|
111
|
+
}
|
|
112
|
+
return collect(read.file, env, now);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* W9: the permission baseline is the ceiling. `network` is decided by the
|
|
116
|
+
* baseline; `write_roots` is dropped (with a warning) when the baseline allows
|
|
117
|
+
* no writes at all (read-only); reads/hosts stay additive; `unsandboxed` is
|
|
118
|
+
* `preset.unsandboxed && the operator env gate` (decision W9-b).
|
|
119
|
+
*/
|
|
120
|
+
function intersectGrants(grants, permission, env, sessionDisabled = []) {
|
|
121
|
+
const warnings = [];
|
|
122
|
+
/**
|
|
123
|
+
* W864: an `allPaths` baseline replaces BOTH root lists with the filesystem
|
|
124
|
+
* root. Both sides are required: `writeRoots` alone would leave the path
|
|
125
|
+
* guard denying `read_file`/`list_dir`, and bwrap without the rw bind.
|
|
126
|
+
* Scoping note: this is the ONLY cap `allPaths` moves — network, net_hosts,
|
|
127
|
+
* tool_extra, unsandboxed and the W860 toolDeny union keep their own rules.
|
|
128
|
+
*/
|
|
129
|
+
const allPaths = permission.allPaths === true;
|
|
130
|
+
const writesAllowed = allPaths || permission.workspaceWritable || permission.toolRootsWritable || permission.writeRoots.length > 0;
|
|
131
|
+
if (!writesAllowed && grants.writeRoots.length > 0)
|
|
132
|
+
warnings.push("write_roots grant ignored: the session permission is read-only");
|
|
133
|
+
return {
|
|
134
|
+
grants: {
|
|
135
|
+
network: permission.network,
|
|
136
|
+
readRoots: allPaths ? ["/"] : grants.readRoots,
|
|
137
|
+
writeRoots: allPaths ? ["/"] : writesAllowed ? [...new Set([...permission.writeRoots, ...grants.writeRoots])] : [],
|
|
138
|
+
netHosts: grants.netHosts,
|
|
139
|
+
toolExtra: grants.toolExtra,
|
|
140
|
+
unsandboxed: (permission.unsandboxed || grants.unsandboxed) && unsandboxedAvailable(env),
|
|
141
|
+
workspaceWritable: permission.workspaceWritable,
|
|
142
|
+
// W860: preset deny first, session-level deny second, deduped. Both are
|
|
143
|
+
// pure SUBTRACTION, so a name the `execution` mode already folded away can
|
|
144
|
+
// never come back through this list (engine-plugins keeps them blocked).
|
|
145
|
+
toolDeny: [...new Set([...permission.toolDeny, ...sessionDisabled])],
|
|
146
|
+
sources: grants.sources,
|
|
147
|
+
},
|
|
148
|
+
warnings,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Fold the (already shape-checked) entries into the effective set. */
|
|
152
|
+
function collect(file, env, now) {
|
|
153
|
+
const warnings = [];
|
|
154
|
+
const acc = { readRoots: [], writeRoots: [], netHosts: [], toolExtra: [], sources: [] };
|
|
155
|
+
const ctx = { env, known: knownSecretsOf(env), readRoots: envReadRoots(env) };
|
|
156
|
+
for (const grant of file.grants) {
|
|
157
|
+
if (isExpired(grant, now)) {
|
|
158
|
+
warnings.push(`grant ${grant.id} (${grant.cap}) has expired — ignored`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
applyGrant(acc, grant, ctx, warnings);
|
|
162
|
+
}
|
|
163
|
+
return { grants: { ...acc, network: acc.network === true, unsandboxed: acc.unsandboxed === true, workspaceWritable: true, toolDeny: [] }, warnings };
|
|
164
|
+
}
|
|
165
|
+
/** `entry`, or a placeholder when the value must not be echoed (§5.4). */
|
|
166
|
+
function show(entry, ctx) {
|
|
167
|
+
return looksLikeCredential(entry, ctx.known) ? "<value looks like a credential>" : entry;
|
|
168
|
+
}
|
|
169
|
+
function applyGrant(acc, grant, ctx, warnings) {
|
|
170
|
+
const keep = () => {
|
|
171
|
+
acc.sources.push({ cap: grant.cap, grantId: grant.id, expiresAt: grant.expires_at });
|
|
172
|
+
};
|
|
173
|
+
if (grant.cap === "network") {
|
|
174
|
+
acc.network = true;
|
|
175
|
+
keep();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (grant.cap === "unsandboxed") {
|
|
179
|
+
acc.unsandboxed = true;
|
|
180
|
+
keep();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (grant.cap === "read_roots" || grant.cap === "write_roots") {
|
|
184
|
+
const roots = rootsOf(grant, { ...ctx, readRoots: [...ctx.readRoots, ...acc.readRoots] }, warnings);
|
|
185
|
+
if (roots.length === 0)
|
|
186
|
+
return;
|
|
187
|
+
if (grant.cap === "read_roots")
|
|
188
|
+
acc.readRoots.push(...roots);
|
|
189
|
+
else
|
|
190
|
+
acc.writeRoots.push(...roots);
|
|
191
|
+
keep();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (grant.cap === "net_hosts") {
|
|
195
|
+
const hosts = (grant.scope.hosts ?? []).filter((h) => isHostEntry(h) || isIpEntry(h));
|
|
196
|
+
if (hosts.length === 0) {
|
|
197
|
+
warnings.push(`grant ${grant.id} (net_hosts): no usable host (dropped: ${(grant.scope.hosts ?? []).map((h) => show(h, ctx)).join(", ")}) — ignored`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
acc.netHosts.push(...hosts);
|
|
201
|
+
keep();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const tools = (grant.scope.tools ?? []).filter((t) => /^[a-z][a-z0-9_]{0,63}$/.test(t));
|
|
205
|
+
if (tools.length === 0) {
|
|
206
|
+
warnings.push(`grant ${grant.id} (tool_extra) holds no usable tool name — ignored`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
acc.toolExtra.push(...tools);
|
|
210
|
+
keep();
|
|
211
|
+
}
|
|
212
|
+
/** The six path rules of §4.3.3 — ALL must hold, else the whole entry is dropped. */
|
|
213
|
+
function rootsOf(grant, ctx, warnings) {
|
|
214
|
+
const raw = grant.scope.roots ?? [];
|
|
215
|
+
const canonical = [];
|
|
216
|
+
for (const entry of raw) {
|
|
217
|
+
const rejected = rejectRoot(entry, { ...ctx, readRoots: [...ctx.readRoots, ...canonical] }, grant);
|
|
218
|
+
if (rejected !== null) {
|
|
219
|
+
warnings.push(`grant ${grant.id} (${grant.cap}): ${rejected} — the entry is ignored`);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
const resolved = canonicalPath(entry);
|
|
223
|
+
if (resolved !== null)
|
|
224
|
+
canonical.push(resolved);
|
|
225
|
+
}
|
|
226
|
+
return canonical.length === 0 ? [] : [...new Set(canonical)];
|
|
227
|
+
}
|
|
228
|
+
/** `null` = usable; otherwise the reason (never echoing a credential value). */
|
|
229
|
+
function rejectRoot(entry, ctx, grant) {
|
|
230
|
+
if (looksLikeCredential(entry, ctx.known))
|
|
231
|
+
return "the value looks like a credential";
|
|
232
|
+
if (!isAbsolute(entry))
|
|
233
|
+
return `root '${show(entry, ctx)}' is not absolute`;
|
|
234
|
+
const resolved = canonicalPath(entry);
|
|
235
|
+
if (resolved === null)
|
|
236
|
+
return `root '${show(entry, ctx)}' does not exist`;
|
|
237
|
+
if (!isDirectory(resolved))
|
|
238
|
+
return `root '${show(entry, ctx)}' is not a directory`;
|
|
239
|
+
if (resolved === "/")
|
|
240
|
+
return "root '/' is the filesystem root";
|
|
241
|
+
const dataDir = canonicalPath(dirname(loadStudioConfig({ env: ctx.env }).paths.workspacesFile));
|
|
242
|
+
if (dataDir !== null && (isInside(dataDir, resolved) || dataDir === resolved))
|
|
243
|
+
return "root covers the studio data directory";
|
|
244
|
+
const home = canonicalPath(ctx.env["HOME"] ?? homedir());
|
|
245
|
+
if (home !== null && resolved === home)
|
|
246
|
+
return "root is $HOME";
|
|
247
|
+
if (grant.cap === "write_roots") {
|
|
248
|
+
const clash = ctx.readRoots.find((root) => isInside(resolved, root) || isInside(root, resolved));
|
|
249
|
+
if (clash !== undefined)
|
|
250
|
+
return `write root overlaps the read root '${clash}'`;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function canonicalPath(path) {
|
|
255
|
+
try {
|
|
256
|
+
return realpathSync(path);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function isDirectory(path) {
|
|
263
|
+
try {
|
|
264
|
+
return statSync(path).isDirectory();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Env-declared read roots, canonicalized best-effort (grant overlap check). */
|
|
271
|
+
export function envReadRoots(env) {
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const entry of parseToolRoots(env["CELESTEA_TOOL_ROOTS"])) {
|
|
274
|
+
const resolved = canonicalPath(entry);
|
|
275
|
+
if (resolved !== null)
|
|
276
|
+
out.push(resolved);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
function isIpEntry(entry) {
|
|
281
|
+
try {
|
|
282
|
+
parseIpRange(entry);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function isHostEntry(entry) {
|
|
290
|
+
return /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(entry);
|
|
291
|
+
}
|
|
292
|
+
/** Cap names currently in force (never paths) — `GET /api/status` (§5.7). */
|
|
293
|
+
/**
|
|
294
|
+
* W9: validate a CUSTOM permission preset, reusing the grants root rules
|
|
295
|
+
* (`rejectRoot`) so a preset can never declare a root an equivalent
|
|
296
|
+
* `write_roots` grant would have been refused (data dir, $HOME, `/`,
|
|
297
|
+
* non-absolute, non-existent, credential-shaped, read-root overlap).
|
|
298
|
+
* Returns `conflict: true` for a built-in or already-taken id.
|
|
299
|
+
*/
|
|
300
|
+
export function validatePermissionPreset(raw, env, existingIds) {
|
|
301
|
+
const parsed = parsePreset(raw);
|
|
302
|
+
if (parsed === null)
|
|
303
|
+
return { ok: false, error: "preset must be an object with a valid id ([a-z][a-z0-9_-]{0,63})" };
|
|
304
|
+
if (isBuiltinPresetId(parsed.id) || existingIds.includes(parsed.id)) {
|
|
305
|
+
return { ok: false, error: "preset '" + parsed.id + "' already exists", conflict: true };
|
|
306
|
+
}
|
|
307
|
+
const ctx = { env, known: knownSecretsOf(env), readRoots: envReadRoots(env) };
|
|
308
|
+
const roots = [];
|
|
309
|
+
for (const entry of parsed.writeRoots) {
|
|
310
|
+
const rejected = rejectRoot(entry, ctx, { cap: "write_roots" });
|
|
311
|
+
if (rejected !== null)
|
|
312
|
+
return { ok: false, error: "write root '" + entry + "': " + rejected };
|
|
313
|
+
const resolved = canonicalPath(entry);
|
|
314
|
+
if (resolved !== null && !roots.includes(resolved))
|
|
315
|
+
roots.push(resolved);
|
|
316
|
+
}
|
|
317
|
+
return { ok: true, preset: { ...parsed, writeRoots: roots } };
|
|
318
|
+
}
|
|
319
|
+
export function grantsActiveCaps(grants) {
|
|
320
|
+
const caps = [];
|
|
321
|
+
if (grants.network)
|
|
322
|
+
caps.push("network");
|
|
323
|
+
if (grants.readRoots.length > 0)
|
|
324
|
+
caps.push("read_roots");
|
|
325
|
+
if (grants.writeRoots.length > 0)
|
|
326
|
+
caps.push("write_roots");
|
|
327
|
+
if (grants.netHosts.length > 0)
|
|
328
|
+
caps.push("net_hosts");
|
|
329
|
+
if (grants.toolExtra.length > 0)
|
|
330
|
+
caps.push("tool_extra");
|
|
331
|
+
if (grants.unsandboxed)
|
|
332
|
+
caps.push("unsandboxed");
|
|
333
|
+
return caps;
|
|
334
|
+
}
|
|
335
|
+
/** Grant ids that are past their `expires_at` (the `expire` audit event). */
|
|
336
|
+
export function expiredGrants(file, now) {
|
|
337
|
+
return file.grants.filter((grant) => isExpired(grant, now));
|
|
338
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effectivePermissionOf (W9) - the ONE reader of a session's permission
|
|
3
|
+
* BASELINE. engine-grants.ts intersects grants into it.
|
|
4
|
+
*
|
|
5
|
+
* Resolution of the requested preset:
|
|
6
|
+
* 1. the caller hint (worker sessions have no directory and carry it in meta);
|
|
7
|
+
* 2. <session dir>/permission.json;
|
|
8
|
+
* 3. CELESTEA_PERMISSION_DEFAULT (default "full-access").
|
|
9
|
+
* The baseline capabilities are then CLAMPED by CELESTEA_PERMISSION_MAX
|
|
10
|
+
* (default "full-access"). The operator env gate for "unsandboxed" is applied
|
|
11
|
+
* by engine-grants.ts when it builds the final EffectiveGrants, so this module
|
|
12
|
+
* never imports engine-grants (no cycle).
|
|
13
|
+
*/
|
|
14
|
+
export interface PermissionBaseline {
|
|
15
|
+
preset: string;
|
|
16
|
+
network: boolean;
|
|
17
|
+
workspaceWritable: boolean;
|
|
18
|
+
toolRootsWritable: boolean;
|
|
19
|
+
writeRoots: readonly string[];
|
|
20
|
+
/** W864: the whole filesystem is readable + writable (never env-gated). */
|
|
21
|
+
allPaths: boolean;
|
|
22
|
+
/** The preset capability; the env gate is applied by engine-grants.ts. */
|
|
23
|
+
unsandboxed: boolean;
|
|
24
|
+
toolDeny: readonly string[];
|
|
25
|
+
warnings: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** The data dir of the studio (same one workspaces.json / auth secret use). */
|
|
28
|
+
export declare function permissionDataDir(env: NodeJS.ProcessEnv): string;
|
|
29
|
+
export declare function effectivePermissionOf(sessionDir: string | null, sessionId: string | null, env: NodeJS.ProcessEnv, presetHint?: string | null): PermissionBaseline;
|