|
27 | 27 | "Limits", |
28 | 28 | "Output", |
29 | 29 | "FileInfo", |
| 30 | + "ShellConfig", |
| 31 | + "ConfigBind", |
| 32 | + "ConfigCred", |
| 33 | + "ConfigLimits", |
30 | 34 | "ShellError", |
31 | 35 | "FileNotFoundError", |
32 | 36 | "PermissionDeniedError", |
@@ -99,6 +103,120 @@ class Limits: |
99 | 103 | max_depth: int = 64 |
100 | 104 |
|
101 | 105 |
|
| 106 | +# --------------------------------------------------------------------------- # |
| 107 | +# Read-only config snapshot (returned by ``Shell.config``) |
| 108 | +# --------------------------------------------------------------------------- # |
| 109 | + |
| 110 | + |
| 111 | +@dataclass(frozen=True) |
| 112 | +class ConfigBind: |
| 113 | + """A bind mount as reported by :attr:`Shell.config`. |
| 114 | +
|
| 115 | + Read-only view; mirrors the :class:`Bind` you pass in, with ``mode`` |
| 116 | + normalized to ``"copy"`` / ``"direct"``. |
| 117 | + """ |
| 118 | + |
| 119 | + source: str |
| 120 | + destination: str |
| 121 | + mode: Literal["direct", "copy"] |
| 122 | + readonly: bool |
| 123 | + |
| 124 | + |
| 125 | +@dataclass(frozen=True) |
| 126 | +class ConfigCred: |
| 127 | + """A credential rule as reported by :attr:`Shell.config`. |
| 128 | +
|
| 129 | + The secret value is **never** exposed. ``env_var`` holds the environment |
| 130 | + variable name when the credential was configured from the environment; |
| 131 | + ``from_literal`` is ``True`` when a literal token was supplied directly |
| 132 | + (its value is still withheld). |
| 133 | + """ |
| 134 | + |
| 135 | + url: str |
| 136 | + kind: str |
| 137 | + methods: tuple[str, ...] |
| 138 | + param: str | None |
| 139 | + env_var: str | None |
| 140 | + from_literal: bool |
| 141 | + |
| 142 | + |
| 143 | +@dataclass(frozen=True) |
| 144 | +class ConfigLimits: |
| 145 | + """Resource caps as reported by :attr:`Shell.config`. |
| 146 | +
|
| 147 | + Unlike :class:`Limits` (the input bundle), this view always carries every |
| 148 | + cap with its concrete active value. |
| 149 | + """ |
| 150 | + |
| 151 | + max_depth: int |
| 152 | + max_output: int |
| 153 | + max_fds: int |
| 154 | + max_bg_jobs: int |
| 155 | + max_pipeline: int |
| 156 | + max_input: int |
| 157 | + max_file_size: int |
| 158 | + max_inodes: int |
| 159 | + |
| 160 | + |
| 161 | +@dataclass(frozen=True) |
| 162 | +class ShellConfig: |
| 163 | + """A read-only snapshot of how a :class:`Shell` was configured. |
| 164 | +
|
| 165 | + Returned by :attr:`Shell.config`. Lets an embedder introspect a constructed |
| 166 | + shell after the fact (to build tool descriptions, surface the network |
| 167 | + allowlist, or report active limits) without having held onto the |
| 168 | + construction arguments. Secret values are never included. |
| 169 | + """ |
| 170 | + |
| 171 | + binds: tuple[ConfigBind, ...] |
| 172 | + credentials: tuple[ConfigCred, ...] |
| 173 | + allowed_urls: tuple[str, ...] |
| 174 | + env: dict[str, str] |
| 175 | + umask: int |
| 176 | + timeout: float | None |
| 177 | + limits: ConfigLimits |
| 178 | + |
| 179 | + |
| 180 | +def _snapshot_from_native(native_config: object) -> ShellConfig: |
| 181 | + """Convert a ``_native.ShellConfig`` into the frozen public dataclass.""" |
| 182 | + return ShellConfig( |
| 183 | + binds=tuple( |
| 184 | + ConfigBind( |
| 185 | + source=b.source, |
| 186 | + destination=b.destination, |
| 187 | + mode=b.mode, # type: ignore[arg-type] |
| 188 | + readonly=b.readonly, |
| 189 | + ) |
| 190 | + for b in native_config.binds |
| 191 | + ), |
| 192 | + credentials=tuple( |
| 193 | + ConfigCred( |
| 194 | + url=c.url, |
| 195 | + kind=c.kind, |
| 196 | + methods=tuple(c.methods), |
| 197 | + param=c.param, |
| 198 | + env_var=c.env_var, |
| 199 | + from_literal=c.from_literal, |
| 200 | + ) |
| 201 | + for c in native_config.credentials |
| 202 | + ), |
| 203 | + allowed_urls=tuple(native_config.allowed_urls), |
| 204 | + env=dict(native_config.env), |
| 205 | + umask=native_config.umask, |
| 206 | + timeout=native_config.timeout, |
| 207 | + limits=ConfigLimits( |
| 208 | + max_depth=native_config.limits.max_depth, |
| 209 | + max_output=native_config.limits.max_output, |
| 210 | + max_fds=native_config.limits.max_fds, |
| 211 | + max_bg_jobs=native_config.limits.max_bg_jobs, |
| 212 | + max_pipeline=native_config.limits.max_pipeline, |
| 213 | + max_input=native_config.limits.max_input, |
| 214 | + max_file_size=native_config.limits.max_file_size, |
| 215 | + max_inodes=native_config.limits.max_inodes, |
| 216 | + ), |
| 217 | + ) |
| 218 | + |
| 219 | + |
102 | 220 | # --------------------------------------------------------------------------- # |
103 | 221 | # Exception hierarchy |
104 | 222 | # --------------------------------------------------------------------------- # |
@@ -251,6 +369,23 @@ def set_env(self, key: str, value: str) -> None: |
251 | 369 | def get_env(self, key: str) -> str | None: |
252 | 370 | return self._shell.get_env(key) |
253 | 371 |
|
| 372 | + # ---- Configuration introspection ---- |
| 373 | + |
| 374 | + @property |
| 375 | + def config(self) -> ShellConfig: |
| 376 | + """A read-only snapshot of how this shell was configured. |
| 377 | +
|
| 378 | + Reports bind mounts, credential rules, the network allowlist, seeded |
| 379 | + environment variables, umask, timeout, and resource caps. Useful for |
| 380 | + introspecting a constructed shell — e.g. to build tool descriptions or |
| 381 | + surface the allowlist — without having held onto the constructor args. |
| 382 | +
|
| 383 | + Secret values are never included: each :class:`ConfigCred` reports its |
| 384 | + URL pattern, kind, and source (literal vs environment variable name), |
| 385 | + but never the token itself. |
| 386 | + """ |
| 387 | + return _snapshot_from_native(self._shell.config()) |
| 388 | + |
254 | 389 | # ---- VFS file operations ---- |
255 | 390 | # Each accepts **kwargs and ignores unknown keys, matching the |
256 | 391 | # kwargs-tolerant strands.sandbox.Sandbox contract the adapter passes |
|
0 commit comments