Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,47 @@ shell = strands_shell.Shell(

> ⚠️ **`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.

### Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This
is useful when you embed Strands Shell as a sandbox in a larger framework and
need to build tool descriptions, surface the network allowlist, or report the
active resource caps from a shell object you were handed.

```python
shell = strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)

cfg = shell.config # a frozen ShellConfig snapshot
cfg.allowed_urls # ('https://api.example.com/',)
cfg.timeout # 30.0
cfg.credentials[0].url # 'https://api.example.com/'
cfg.credentials[0].env_var # 'API_TOKEN' (the secret value is never exposed)
```

```javascript
const shell = await Shell.create({
allowedUrls: ['https://api.example.com/'],
credentials: [{ url: 'https://api.example.com/', envVar: 'API_TOKEN' }],
timeout: 30,
})

const cfg = await shell.config() // a deep-frozen snapshot object
cfg.allowedUrls // ['https://api.example.com/']
cfg.timeout // 30
cfg.credentials[0].envVar // 'API_TOKEN' (the secret value is never exposed)
```

The snapshot reports binds, credentials, the network allowlist, environment
variables, umask, timeout, and resource limits. Credential **secrets are never
included** — each entry reports its URL pattern, kind, and source (a literal
token was supplied, or the name of the environment variable it is read from),
so you can reason about credentials without the agent or your tooling ever
seeing the secret itself.

### TOML

You can load all of this from a config file instead:
Expand Down
60 changes: 60 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,61 @@ export interface ShellConfig {
configFile?: string
}

/** A bind mount in a {@link ShellConfigSnapshot}. */
export interface BindInfo {
readonly source: string
readonly destination: string
/** `'copy'` (build-time snapshot) or `'direct'` (host passthrough). */
readonly mode: 'copy' | 'direct'
readonly readonly: boolean
}

/**
* A credential rule in a {@link ShellConfigSnapshot}.
*
* The secret value is never exposed. `envVar` holds the environment-variable
* name when the credential was configured from the environment; `fromLiteral`
* is `true` when a literal token was supplied directly (its value is withheld).
*/
export interface CredInfo {
readonly url: string
readonly kind: 'bearer' | 'query'
readonly methods: readonly string[]
readonly param: string | null
readonly envVar: string | null
readonly fromLiteral: boolean
}

/** Resource caps in a {@link ShellConfigSnapshot}. Every cap is present. */
export interface LimitsInfo {
readonly maxDepth: number
readonly maxOutput: number
readonly maxFds: number
readonly maxBgJobs: number
readonly maxPipeline: number
readonly maxInput: number
readonly maxFileSize: number
readonly maxInodes: number
}

/**
* A read-only snapshot of how a {@link Shell} was configured, returned by
* {@link Shell.config}. Lets an embedder introspect a constructed shell — to
* build tool descriptions, surface the network allowlist, or report active
* limits — without having held onto the {@link ShellConfig} it was built from.
* Secret values are never included.
*/
export interface ShellConfigSnapshot {
readonly binds: readonly BindInfo[]
readonly credentials: readonly CredInfo[]
readonly allowedUrls: readonly string[]
readonly env: Readonly<Record<string, string>>
readonly umask: number
/** Per-command timeout in seconds, or `null` for no timeout. */
readonly timeout: number | null
readonly limits: LimitsInfo
}

/** errno-style discriminator carried on every {@link ShellError}. */
export type ShellErrorCode = 'ENOENT' | 'EACCES' | 'EFBIG' | 'EOTHER'

Expand All @@ -89,6 +144,11 @@ export declare class Shell {
setEnv(key: string, value: string): Promise<void>
/** Get an environment variable. */
getEnv(key: string): Promise<string | null>
/**
* A read-only snapshot of how this shell was configured. Secret values are
* never included — see {@link CredInfo}. The returned object is deep-frozen.
*/
config(): Promise<ShellConfigSnapshot>
/** Read a file as raw bytes. Rejects with {@link NotFoundError} if missing. */
readFile(path: string): Promise<Uint8Array>
/** Write raw bytes; creates parent dirs (mkdir -p) and truncates. */
Expand Down
46 changes: 46 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,52 @@ class Shell {
return this._inner.getEnv(key)
}

// ---- Configuration introspection ----

/**
* A read-only snapshot of how this shell was configured. Resolves to an
* object reporting binds, credentials, allowedUrls, env, umask, timeout,
* and limits.
*
* Secret values are never included: each credential reports its `url`
* pattern, `kind`, and source (`envVar` name, or `fromLiteral: true`), but
* never the token itself. The returned object and its nested objects/arrays
* are deep-frozen so the snapshot cannot be mutated.
*/
async config() {
const c = await this._inner.config()
const snapshot = {
binds: c.binds.map((b) =>
Object.freeze({
source: b.source,
destination: b.destination,
mode: b.mode,
readonly: b.readonly,
}),
),
credentials: c.credentials.map((cr) =>
Object.freeze({
url: cr.url,
kind: cr.kind,
methods: Object.freeze([...cr.methods]),
param: cr.param ?? null,
envVar: cr.envVar ?? null,
fromLiteral: cr.fromLiteral,
}),
),
allowedUrls: c.allowedUrls,
env: c.env,
umask: c.umask,
timeout: c.timeout ?? null,
limits: Object.freeze({ ...c.limits }),
}
Object.freeze(snapshot.binds)
Object.freeze(snapshot.credentials)
Object.freeze(snapshot.allowedUrls)
Object.freeze(snapshot.env)
return Object.freeze(snapshot)
}

// ---- VFS file operations ----

readFile(path) {
Expand Down
135 changes: 135 additions & 0 deletions python/strands_shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"Limits",
"Output",
"FileInfo",
"ShellConfig",
"ConfigBind",
"ConfigCred",
"ConfigLimits",
"ShellError",
"FileNotFoundError",
"PermissionDeniedError",
Expand Down Expand Up @@ -99,6 +103,120 @@ class Limits:
max_depth: int = 64


# --------------------------------------------------------------------------- #
# Read-only config snapshot (returned by ``Shell.config``)
# --------------------------------------------------------------------------- #


@dataclass(frozen=True)
class ConfigBind:
"""A bind mount as reported by :attr:`Shell.config`.

Read-only view; mirrors the :class:`Bind` you pass in, with ``mode``
normalized to ``"copy"`` / ``"direct"``.
"""

source: str
destination: str
mode: Literal["direct", "copy"]
readonly: bool


@dataclass(frozen=True)
class ConfigCred:
"""A credential rule as reported by :attr:`Shell.config`.

The secret value is **never** exposed. ``env_var`` holds the environment
variable name when the credential was configured from the environment;
``from_literal`` is ``True`` when a literal token was supplied directly
(its value is still withheld).
"""

url: str
kind: str
methods: tuple[str, ...]
param: str | None
env_var: str | None
from_literal: bool


@dataclass(frozen=True)
class ConfigLimits:
"""Resource caps as reported by :attr:`Shell.config`.

Unlike :class:`Limits` (the input bundle), this view always carries every
cap with its concrete active value.
"""

max_depth: int
max_output: int
max_fds: int
max_bg_jobs: int
max_pipeline: int
max_input: int
max_file_size: int
max_inodes: int


@dataclass(frozen=True)
class ShellConfig:
"""A read-only snapshot of how a :class:`Shell` was configured.

Returned by :attr:`Shell.config`. Lets an embedder introspect a constructed
shell after the fact (to build tool descriptions, surface the network
allowlist, or report active limits) without having held onto the
construction arguments. Secret values are never included.
"""

binds: tuple[ConfigBind, ...]
credentials: tuple[ConfigCred, ...]
allowed_urls: tuple[str, ...]
env: dict[str, str]
umask: int
timeout: float | None
limits: ConfigLimits


def _snapshot_from_native(native_config: object) -> ShellConfig:
"""Convert a ``_native.ShellConfig`` into the frozen public dataclass."""
return ShellConfig(
binds=tuple(
ConfigBind(
source=b.source,
destination=b.destination,
mode=b.mode, # type: ignore[arg-type]
readonly=b.readonly,
)
for b in native_config.binds
),
credentials=tuple(
ConfigCred(
url=c.url,
kind=c.kind,
methods=tuple(c.methods),
param=c.param,
env_var=c.env_var,
from_literal=c.from_literal,
)
for c in native_config.credentials
),
allowed_urls=tuple(native_config.allowed_urls),
env=dict(native_config.env),
umask=native_config.umask,
timeout=native_config.timeout,
limits=ConfigLimits(
max_depth=native_config.limits.max_depth,
max_output=native_config.limits.max_output,
max_fds=native_config.limits.max_fds,
max_bg_jobs=native_config.limits.max_bg_jobs,
max_pipeline=native_config.limits.max_pipeline,
max_input=native_config.limits.max_input,
max_file_size=native_config.limits.max_file_size,
max_inodes=native_config.limits.max_inodes,
),
)


# --------------------------------------------------------------------------- #
# Exception hierarchy
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -251,6 +369,23 @@ def set_env(self, key: str, value: str) -> None:
def get_env(self, key: str) -> str | None:
return self._shell.get_env(key)

# ---- Configuration introspection ----

@property
def config(self) -> ShellConfig:
"""A read-only snapshot of how this shell was configured.

Reports bind mounts, credential rules, the network allowlist, seeded
environment variables, umask, timeout, and resource caps. Useful for
introspecting a constructed shell — e.g. to build tool descriptions or
surface the allowlist — without having held onto the constructor args.

Secret values are never included: each :class:`ConfigCred` reports its
URL pattern, kind, and source (literal vs environment variable name),
but never the token itself.
"""
return _snapshot_from_native(self._shell.config())

# ---- VFS file operations ----
# Each accepts **kwargs and ignores unknown keys, matching the
# kwargs-tolerant strands.sandbox.Sandbox contract the adapter passes
Expand Down
Loading
Loading