Skip to content

Commit 2d5d5cb

Browse files
feat: expose read-only config snapshot via Shell.config (#36)
Co-authored-by: agent-of-mkmeral <agent-of-mkmeral@users.noreply.github.com> Co-authored-by: Murat Kaan Meral <muratkaanmeral@gmail.com>
1 parent 7478516 commit 2d5d5cb

13 files changed

Lines changed: 1289 additions & 4 deletions

File tree

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,47 @@ shell = strands_shell.Shell(
141141

142142
> ⚠️ **`mode: "direct"` mounts are live.** The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.
143143
144+
### Inspecting configuration
145+
146+
A constructed shell exposes a read-only snapshot of how it was configured. This
147+
is useful when you embed Strands Shell as a sandbox in a larger framework and
148+
need to build tool descriptions, surface the network allowlist, or report the
149+
active resource caps from a shell object you were handed.
150+
151+
```python
152+
shell = strands_shell.Shell(
153+
allowed_urls=["https://api.example.com/"],
154+
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
155+
timeout=30.0,
156+
)
157+
158+
cfg = shell.config # a frozen ShellConfig snapshot
159+
cfg.allowed_urls # ('https://api.example.com/',)
160+
cfg.timeout # 30.0
161+
cfg.credentials[0].url # 'https://api.example.com/'
162+
cfg.credentials[0].env_var # 'API_TOKEN' (the secret value is never exposed)
163+
```
164+
165+
```javascript
166+
const shell = await Shell.create({
167+
allowedUrls: ['https://api.example.com/'],
168+
credentials: [{ url: 'https://api.example.com/', envVar: 'API_TOKEN' }],
169+
timeout: 30,
170+
})
171+
172+
const cfg = await shell.config() // a deep-frozen snapshot object
173+
cfg.allowedUrls // ['https://api.example.com/']
174+
cfg.timeout // 30
175+
cfg.credentials[0].envVar // 'API_TOKEN' (the secret value is never exposed)
176+
```
177+
178+
The snapshot reports binds, credentials, the network allowlist, environment
179+
variables, umask, timeout, and resource limits. Credential **secrets are never
180+
included** — each entry reports its URL pattern, kind, and source (a literal
181+
token was supplied, or the name of the environment variable it is read from),
182+
so you can reason about credentials without the agent or your tooling ever
183+
seeing the secret itself.
184+
144185
### TOML
145186

146187
You can load all of this from a config file instead:

index.d.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,61 @@ export interface ShellConfig {
6363
configFile?: string
6464
}
6565

66+
/** A bind mount in a {@link ShellConfigSnapshot}. */
67+
export interface BindInfo {
68+
readonly source: string
69+
readonly destination: string
70+
/** `'copy'` (build-time snapshot) or `'direct'` (host passthrough). */
71+
readonly mode: 'copy' | 'direct'
72+
readonly readonly: boolean
73+
}
74+
75+
/**
76+
* A credential rule in a {@link ShellConfigSnapshot}.
77+
*
78+
* The secret value is never exposed. `envVar` holds the environment-variable
79+
* name when the credential was configured from the environment; `fromLiteral`
80+
* is `true` when a literal token was supplied directly (its value is withheld).
81+
*/
82+
export interface CredInfo {
83+
readonly url: string
84+
readonly kind: 'bearer' | 'query'
85+
readonly methods: readonly string[]
86+
readonly param: string | null
87+
readonly envVar: string | null
88+
readonly fromLiteral: boolean
89+
}
90+
91+
/** Resource caps in a {@link ShellConfigSnapshot}. Every cap is present. */
92+
export interface LimitsInfo {
93+
readonly maxDepth: number
94+
readonly maxOutput: number
95+
readonly maxFds: number
96+
readonly maxBgJobs: number
97+
readonly maxPipeline: number
98+
readonly maxInput: number
99+
readonly maxFileSize: number
100+
readonly maxInodes: number
101+
}
102+
103+
/**
104+
* A read-only snapshot of how a {@link Shell} was configured, returned by
105+
* {@link Shell.config}. Lets an embedder introspect a constructed shell — to
106+
* build tool descriptions, surface the network allowlist, or report active
107+
* limits — without having held onto the {@link ShellConfig} it was built from.
108+
* Secret values are never included.
109+
*/
110+
export interface ShellConfigSnapshot {
111+
readonly binds: readonly BindInfo[]
112+
readonly credentials: readonly CredInfo[]
113+
readonly allowedUrls: readonly string[]
114+
readonly env: Readonly<Record<string, string>>
115+
readonly umask: number
116+
/** Per-command timeout in seconds, or `null` for no timeout. */
117+
readonly timeout: number | null
118+
readonly limits: LimitsInfo
119+
}
120+
66121
/** errno-style discriminator carried on every {@link ShellError}. */
67122
export type ShellErrorCode = 'ENOENT' | 'EACCES' | 'EFBIG' | 'EOTHER'
68123

@@ -89,6 +144,11 @@ export declare class Shell {
89144
setEnv(key: string, value: string): Promise<void>
90145
/** Get an environment variable. */
91146
getEnv(key: string): Promise<string | null>
147+
/**
148+
* A read-only snapshot of how this shell was configured. Secret values are
149+
* never included — see {@link CredInfo}. The returned object is deep-frozen.
150+
*/
151+
config(): Promise<ShellConfigSnapshot>
92152
/** Read a file as raw bytes. Rejects with {@link NotFoundError} if missing. */
93153
readFile(path: string): Promise<Uint8Array>
94154
/** Write raw bytes; creates parent dirs (mkdir -p) and truncates. */

index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,52 @@ class Shell {
190190
return this._inner.getEnv(key)
191191
}
192192

193+
// ---- Configuration introspection ----
194+
195+
/**
196+
* A read-only snapshot of how this shell was configured. Resolves to an
197+
* object reporting binds, credentials, allowedUrls, env, umask, timeout,
198+
* and limits.
199+
*
200+
* Secret values are never included: each credential reports its `url`
201+
* pattern, `kind`, and source (`envVar` name, or `fromLiteral: true`), but
202+
* never the token itself. The returned object and its nested objects/arrays
203+
* are deep-frozen so the snapshot cannot be mutated.
204+
*/
205+
async config() {
206+
const c = await this._inner.config()
207+
const snapshot = {
208+
binds: c.binds.map((b) =>
209+
Object.freeze({
210+
source: b.source,
211+
destination: b.destination,
212+
mode: b.mode,
213+
readonly: b.readonly,
214+
}),
215+
),
216+
credentials: c.credentials.map((cr) =>
217+
Object.freeze({
218+
url: cr.url,
219+
kind: cr.kind,
220+
methods: Object.freeze([...cr.methods]),
221+
param: cr.param ?? null,
222+
envVar: cr.envVar ?? null,
223+
fromLiteral: cr.fromLiteral,
224+
}),
225+
),
226+
allowedUrls: c.allowedUrls,
227+
env: c.env,
228+
umask: c.umask,
229+
timeout: c.timeout ?? null,
230+
limits: Object.freeze({ ...c.limits }),
231+
}
232+
Object.freeze(snapshot.binds)
233+
Object.freeze(snapshot.credentials)
234+
Object.freeze(snapshot.allowedUrls)
235+
Object.freeze(snapshot.env)
236+
return Object.freeze(snapshot)
237+
}
238+
193239
// ---- VFS file operations ----
194240

195241
readFile(path) {

python/strands_shell/__init__.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"Limits",
2828
"Output",
2929
"FileInfo",
30+
"ShellConfig",
31+
"ConfigBind",
32+
"ConfigCred",
33+
"ConfigLimits",
3034
"ShellError",
3135
"FileNotFoundError",
3236
"PermissionDeniedError",
@@ -99,6 +103,120 @@ class Limits:
99103
max_depth: int = 64
100104

101105

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+
102220
# --------------------------------------------------------------------------- #
103221
# Exception hierarchy
104222
# --------------------------------------------------------------------------- #
@@ -251,6 +369,23 @@ def set_env(self, key: str, value: str) -> None:
251369
def get_env(self, key: str) -> str | None:
252370
return self._shell.get_env(key)
253371

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+
254389
# ---- VFS file operations ----
255390
# Each accepts **kwargs and ignores unknown keys, matching the
256391
# kwargs-tolerant strands.sandbox.Sandbox contract the adapter passes

0 commit comments

Comments
 (0)