Repository navigation
Releases: raandree/ShellPilot
Release list
v0.4.0-preview0016
[v0.4.0-preview0016]
Security
-
Gate every tool class from one policy.
Set-ShpToolPolicygainsUrl,Mcp
andToolrule kinds besideRead,WriteandShell, each matched
against a resolved form rather than the string the model supplied: a
normalised address, thealias/toola call will dispatch under, and an exact
tool name. A deny beats every matching allow. Name the
RestrictedUnattendedtrust profile to enforce all six kinds at once, so an
unattended run may do only what is listed. AnMcprule gates tool identity,
not the arguments inside the call. -
Keep a Copilot credential out of a backend that is not Copilot. An
alternative backend --ApiBase, the session context,
$env:SHELLPILOT_API_BASE, or a caller-owned request transport - now
resolves no GitHub host, reads no OAuth token from any source, and exchanges
no Copilot session token, inInvoke-ShpandRequest-ShpEmbeddingalike. A
pipeline pointed at its own endpoint therefore needs no GitHub sign-in at
all, andTest-ShpCiReadinessreportsTokenSourceasNotRequired
instead of raising a standing issue. The Copilot backend is unchanged. -
Refuse a Skill or Instruction body whose bytes changed after the caller
approved it. Every file that shapes the model's behaviour is fingerprinted
when it is catalogued and re-checked when it is loaded, so a body swapped
between the description the caller read and the content the model receives is
denied with a reason instead of injected. The result carries
ResourceProvenance- source root, relative path, hash, size, trust and
validation state - for every load attempt, so it is possible to establish
after a surprising answer exactly which bytes the model was given. Nothing is
discovered: every root is still one the caller named. -
Bound a Subagent to what dispatched it.
Invoke-ShpSubagentcan only narrow
the tool policy, redaction policy, tool visibility and budget it inherited,
never widen them, and depth, fan-out, concurrency and duration are capped
before any credential work. A child returns its answer and evidence rather
than its transcript, so its exploration never enters the parent's context
window, and a handed-back capability cannot arrive without the controls it
was given. -
Decide a Tool call, or contain it, from outside the model.
-ToolCallControl
is consulted before dispatch and after a result exists and may allow, deny
with a reason, or modify the arguments or the result;-ExecutionContract
wraps covered dispatch for a caller who supplies containment of their own.
Each stage can only narrow: a control is asked only about a call the Tool
policy already allowed, and arguments a control rewrote are re-checked
against the policy before dispatch. Both take a scriptblock or a command
name, are validated before the first request, and are never discovered from
disk. Neither is a sandbox. -
Reach a remote MCP server only where it was approved to be reached. A
Streamable HTTP attachment validates its endpoint at registration and again
before every request and every redirect, requires HTTPS unless a loopback
opt-in is given, refuses embedded credentials, refuses an address that is not
publicly routable, and pins the approved address set so a later answer -
redirect target or re-resolution - cannot move the connection. The body,
stream events and redirect chain are capped. Authorization is only what the
caller supplies through a header or a per-request credential callback: a 401
is reported by name rather than answered with whatever token is in reach, and
nothing is cached or written to disk. -
Correct embedding backend precedence and prevent a Copilot Session token
from reaching a keyless alternative backend. Environment-selected backends
now use the shared resolver. See embeddings. -
Apply protected environment-assignment checks to colon-bound PowerShell
parameter arguments as well as separate arguments. -
Limit
run_commandto a minimal environment and refuse execution-sensitive
literal environment assignments before child startup, even without a Tool
policy. See command environment. -
Protect Unix
edit_filestaging from creation. Apply the source file
mode when creating the empty temporary file, before any content is copied,
so a private source is not temporarily exposed through default permissions. -
Keep
edit_fileon the target approved by the tool policy. Repointing
the original directory alias after authorization no longer redirects the
edit to a different file. Confirmation names the authorized target. Refuse
special files again after staging so a named-pipe swap cannot block the
final content check. External filesystem races remain outside the tool's
guarantees. -
Tool policy refuses paths when link resolution fails. A missing runtime
API or filesystem error no longer leaves an unresolved path eligible for
Read or Write access. This prevents a junction inside an allowed directory
from bypassing rules for its destination on an unsupported runtime. -
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
Grade agent behaviour without spending anything.
Invoke-ShpEvalruns a case
suite through the real Tool-calling loop with the model replaced by a
scripted transport, so it sends no request, reads no credential and exchanges
no token, and a behaviour regression is caught by the ordinary test gate
rather than by a credentialed job. Outcome, trajectory and cost are graded
separately, because an agent that reached the right answer by running a
forbidden command has still failed. A grader this module does not implement
is an error rather than a pass, reliability is reported over repeated trials
instead of a single green run, and credentialed live canaries are skipped
unless they are explicitly asked for. -
Add
Invoke-ShpSubagentfor dispatching part of a task to a child turn with
its own model, reasoning effort, tool set and system prompt, read from an
agent definition file the caller names. The child's answer and evidence come
back, never its transcript, so a broad exploration costs the parent an answer
instead of a conversation. The whole tree shares one budget ledger. -
Add
-ToolCallControland-ExecutionContracttoInvoke-Shpand
Invoke-ShpBatch. A control receives one typed, versioned, independent
request per Tool call - identifiers that correlate with the Event stream, the
tool and its provenance, the original and effective arguments, and the result
in the post phase - and answers with a decision. Every decision is recorded
on the result'sToolCallDecisionsas a receipt and as atool.decision
Event, so an unattended run can show afterwards what was approved, what was
rewritten and what was refused. -
Add
Get-ShpContextReportto price a call before sending it, and
Invoke-Shp -ContextReport/Invoke-ShpBatch -ContextReportto attribute a
call after it. The report breaks the context window down by source - system
prompt, instructions, skills, tool schemas, attachments, history, this turn -
so hitting a context limit says what filled it, and the saving from deferred
tool loading is measurable rather than asserted. The pre-call report sends no
request and needs no credential. -
Add
Compress-ShpChat -Focusto steer compaction: one short instruction
naming what the compression must try to keep, applied as a drop-order
preference over whole exchanges. Anchors, the estimator and the default are
unchanged, and omitting-Focuscompacts exactly as before. -
Add
-ToolResultSpillRoottoInvoke-ShpandInvoke-ShpBatchso an
oversized Tool result is written in full to a caller-named directory and the
model receives a window plus the path, instead of a truncated result it
cannot ask for the rest of. One seam covers every producer, and the spilled
content composes withread_file's existing offset and limit. -
Add
Save-ShpChat,Restore-ShpChatandGet-ShpChatCheckpointto save a
conversation, inspect its checkpoints, resume it, and roll it back to an
earlier turn, plusInvoke-Shp -SaveChatPathto checkpoint a turn as it
completes. Content is written only to a path the caller names - never
discovered, never defaulted - redaction is applied on wr...
v0.4.0-preview0015
[v0.4.0-preview0015]
Security
-
Gate every tool class from one policy.
Set-ShpToolPolicygainsUrl,Mcp
andToolrule kinds besideRead,WriteandShell, each matched
against a resolved form rather than the string the model supplied: a
normalised address, thealias/toola call will dispatch under, and an exact
tool name. A deny beats every matching allow. Name the
RestrictedUnattendedtrust profile to enforce all six kinds at once, so an
unattended run may do only what is listed. AnMcprule gates tool identity,
not the arguments inside the call. -
Keep a Copilot credential out of a backend that is not Copilot. An
alternative backend --ApiBase, the session context,
$env:SHELLPILOT_API_BASE, or a caller-owned request transport - now
resolves no GitHub host, reads no OAuth token from any source, and exchanges
no Copilot session token, inInvoke-ShpandRequest-ShpEmbeddingalike. A
pipeline pointed at its own endpoint therefore needs no GitHub sign-in at
all, andTest-ShpCiReadinessreportsTokenSourceasNotRequired
instead of raising a standing issue. The Copilot backend is unchanged. -
Refuse a Skill or Instruction body whose bytes changed after the caller
approved it. Every file that shapes the model's behaviour is fingerprinted
when it is catalogued and re-checked when it is loaded, so a body swapped
between the description the caller read and the content the model receives is
denied with a reason instead of injected. The result carries
ResourceProvenance- source root, relative path, hash, size, trust and
validation state - for every load attempt, so it is possible to establish
after a surprising answer exactly which bytes the model was given. Nothing is
discovered: every root is still one the caller named. -
Bound a Subagent to what dispatched it.
Invoke-ShpSubagentcan only narrow
the tool policy, redaction policy, tool visibility and budget it inherited,
never widen them, and depth, fan-out, concurrency and duration are capped
before any credential work. A child returns its answer and evidence rather
than its transcript, so its exploration never enters the parent's context
window, and a handed-back capability cannot arrive without the controls it
was given. -
Decide a Tool call, or contain it, from outside the model.
-ToolCallControl
is consulted before dispatch and after a result exists and may allow, deny
with a reason, or modify the arguments or the result;-ExecutionContract
wraps covered dispatch for a caller who supplies containment of their own.
Each stage can only narrow: a control is asked only about a call the Tool
policy already allowed, and arguments a control rewrote are re-checked
against the policy before dispatch. Both take a scriptblock or a command
name, are validated before the first request, and are never discovered from
disk. Neither is a sandbox. -
Reach a remote MCP server only where it was approved to be reached. A
Streamable HTTP attachment validates its endpoint at registration and again
before every request and every redirect, requires HTTPS unless a loopback
opt-in is given, refuses embedded credentials, refuses an address that is not
publicly routable, and pins the approved address set so a later answer -
redirect target or re-resolution - cannot move the connection. The body,
stream events and redirect chain are capped. Authorization is only what the
caller supplies through a header or a per-request credential callback: a 401
is reported by name rather than answered with whatever token is in reach, and
nothing is cached or written to disk. -
Correct embedding backend precedence and prevent a Copilot Session token
from reaching a keyless alternative backend. Environment-selected backends
now use the shared resolver. See embeddings. -
Apply protected environment-assignment checks to colon-bound PowerShell
parameter arguments as well as separate arguments. -
Limit
run_commandto a minimal environment and refuse execution-sensitive
literal environment assignments before child startup, even without a Tool
policy. See command environment. -
Protect Unix
edit_filestaging from creation. Apply the source file
mode when creating the empty temporary file, before any content is copied,
so a private source is not temporarily exposed through default permissions. -
Keep
edit_fileon the target approved by the tool policy. Repointing
the original directory alias after authorization no longer redirects the
edit to a different file. Confirmation names the authorized target. Refuse
special files again after staging so a named-pipe swap cannot block the
final content check. External filesystem races remain outside the tool's
guarantees. -
Tool policy refuses paths when link resolution fails. A missing runtime
API or filesystem error no longer leaves an unresolved path eligible for
Read or Write access. This prevents a junction inside an allowed directory
from bypassing rules for its destination on an unsupported runtime. -
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
Grade agent behaviour without spending anything.
Invoke-ShpEvalruns a case
suite through the real Tool-calling loop with the model replaced by a
scripted transport, so it sends no request, reads no credential and exchanges
no token, and a behaviour regression is caught by the ordinary test gate
rather than by a credentialed job. Outcome, trajectory and cost are graded
separately, because an agent that reached the right answer by running a
forbidden command has still failed. A grader this module does not implement
is an error rather than a pass, reliability is reported over repeated trials
instead of a single green run, and credentialed live canaries are skipped
unless they are explicitly asked for. -
Add
Invoke-ShpSubagentfor dispatching part of a task to a child turn with
its own model, reasoning effort, tool set and system prompt, read from an
agent definition file the caller names. The child's answer and evidence come
back, never its transcript, so a broad exploration costs the parent an answer
instead of a conversation. The whole tree shares one budget ledger. -
Add
-ToolCallControland-ExecutionContracttoInvoke-Shpand
Invoke-ShpBatch. A control receives one typed, versioned, independent
request per Tool call - identifiers that correlate with the Event stream, the
tool and its provenance, the original and effective arguments, and the result
in the post phase - and answers with a decision. Every decision is recorded
on the result'sToolCallDecisionsas a receipt and as atool.decision
Event, so an unattended run can show afterwards what was approved, what was
rewritten and what was refused. -
Add
Get-ShpContextReportto price a call before sending it, and
Invoke-Shp -ContextReport/Invoke-ShpBatch -ContextReportto attribute a
call after it. The report breaks the context window down by source - system
prompt, instructions, skills, tool schemas, attachments, history, this turn -
so hitting a context limit says what filled it, and the saving from deferred
tool loading is measurable rather than asserted. The pre-call report sends no
request and needs no credential. -
Add
Compress-ShpChat -Focusto steer compaction: one short instruction
naming what the compression must try to keep, applied as a drop-order
preference over whole exchanges. Anchors, the estimator and the default are
unchanged, and omitting-Focuscompacts exactly as before. -
Add
-ToolResultSpillRoottoInvoke-ShpandInvoke-ShpBatchso an
oversized Tool result is written in full to a caller-named directory and the
model receives a window plus the path, instead of a truncated result it
cannot ask for the rest of. One seam covers every producer, and the spilled
content composes withread_file's existing offset and limit. -
Add
Save-ShpChat,Restore-ShpChatandGet-ShpChatCheckpointto save a
conversation, inspect its checkpoints, resume it, and roll it back to an
earlier turn, plusInvoke-Shp -SaveChatPathto checkpoint a turn as it
completes. Content is written only to a path the caller names - never
discovered, never defaulted - redaction is applied on wr...
v0.4.0-preview0014
[v0.4.0-preview0014]
Security
-
Correct embedding backend precedence and prevent a Copilot Session token
from reaching a keyless alternative backend. Environment-selected backends
now use the shared resolver. See embeddings. -
Apply protected environment-assignment checks to colon-bound PowerShell
parameter arguments as well as separate arguments. -
Limit
run_commandto a minimal environment and refuse execution-sensitive
literal environment assignments before child startup, even without a Tool
policy. See command environment. -
Protect Unix
edit_filestaging from creation. Apply the source file
mode when creating the empty temporary file, before any content is copied,
so a private source is not temporarily exposed through default permissions. -
Keep
edit_fileon the target approved by the tool policy. Repointing
the original directory alias after authorization no longer redirects the
edit to a different file. Confirmation names the authorized target. Refuse
special files again after staging so a named-pipe swap cannot block the
final content check. External filesystem races remain outside the tool's
guarantees. -
Tool policy refuses paths when link resolution fails. A missing runtime
API or filesystem error no longer leaves an unresolved path eligible for
Read or Write access. This prevents a junction inside an allowed directory
from bypassing rules for its destination on an unsupported runtime. -
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
Add opt-in
-DeferredToolLoadingtoInvoke-ShpandInvoke-ShpBatchfor
Turn-local User/MCP schema search withsearch_tools. Keep fixed built-ins
and explicit-Toolselections eager, with exclusion winning. Reserve the
search_toolsregistration name and add deferred availability/loading result
members. This is a schema-cost option, not authorization, containment, or
prompt-injection defense. See deferred loading. -
Add explicit
-GitHubHost, Session context, andSHELLPILOT_GITHUB_HOST
routing for GitHub Enterprise Cloud on GHE.com, with validated HTTPS origins,
host-specific Session-token caching, and readiness reporting. Preserve
GitHub.com defaults and the bounded child transport allowlist. See
enterprise host configuration. -
Add the per-call
Invoke-Shp -Mode Planread-only preset, intersecting caller
filters and the unchanged session Tool policy. Withhold mutation, terminal,
User, MCP, and interactive-question tools. See
Plan mode and its limits. -
Add exact
-Tooland-ExcludeToolvisibility filters toInvoke-Shpand
Invoke-ShpBatch, covering built-in, User, and MCP names with exclusion
winning. Withdrawn tools retain the existing denial contract. See
tool visibility. -
Add
Set-ShpRedactionPolicy -SecretEnvironmentVariablefor current literal
values in outgoing text and Event records, with names-only reporting and
batch/job replay. See named secret redaction. -
Add the maintainer-selected MIT license and document stable and
prerelease Gallery installation. Bundle the same license
text in built modules and packages. -
Add explicit estimated budgets for trusted owned requests. Opt into
RequestBudgetMode provider-estimatefor non-refundable reservations and
upward reconciliation; default verified counters keep their meaning.
Add the bounded Claude child provider/counting path, pre-generation host
admission, and secret-free reported/partial Usage. No automatic retries,
Model substitution, or invoice guarantee is implied. See
the transport contract. -
Add conditional admission limits to owned
Invoke-Shptransport.
RequestLimitsand a trustedRequestTokenCounterreserve input, maximum
output, and Engine-priced cost before dispatch. Unknown or failed Usage
retains its reservation and stays explicitly unknown in results, events,
and summaries. No verified Copilot counter or complete child runtime is
supplied; ordinary calls are unchanged. See
the contract and limitations. -
Add optional host transport groundwork to
Invoke-Shp. Use
NoAutomaticRetryto disable transport and API-shape resends, or
RequestTransportfor a credentialless Tool-calling loop with a trusted
caller-owned transport. Ordinary calls are unchanged. Hard request admission,
process isolation, and a trusted transport process are not supplied by these
options. See the contract and limitations. -
Add
edit_filefor targeted changes to existing files. Replace one
exact, case-sensitiveoldStringwithnewString, refusing zero matches or
multiple matches with recovery guidance. Preserve the BOM, encoding and
unchanged line endings for UTF-8 and BOM-marked UTF-16/UTF-32; refuse
malformed or unsupported text instead of converting it. The tool is disabled
by-DisableFileAccess, and reports the intended edit without writing under
-WhatIf.When a tool policy is set, an edit needs both a
Read()and aWrite()rule
covering the target, and a deny in either kind refuses it. Match counts
disclose file content even when the replacement is identical, soWrite()
alone would turn the tool into a way to confirm guesses about a file the
policy never granted read access to.write_fileandcreate_directoryare
unchanged and still need onlyWrite().Only regular, seekable files are eligible, and input and output are each
capped at 8 MiB including the BOM. Unix file types are checked directly,
avoiding a hang when formatted permissions misidentify a named pipe as a
regular file. The edit is staged in the target's own
directory, flushed, checked against the file's current bytes, and then put in
place with a backup of the original. A native replacement failure that moves
the original away retains the backup and reportsrecoveryPathfor manual
recovery instead of deleting the remaining copy. Cleanup is best effort.
Detected concurrent changes are refused; the final check and replacement
are not a compare-and-swap guarantee.
See README.md. -
glob_filesandgrep_fileslet the model search without a shell.
glob_filesfinds files by name pattern under a directory;grep_files
searches their contents and returns only the path, line number and matching
line, leavingread_fileto read around a hit. Both are offered with the
other file tools and withdrawn by-DisableFileAccess.Both are governed by the existing
Read()rules ofSet-ShpToolPolicy, and
that is the point: until now the only way to make the model find something
wasrun_command, so a policy tight enough to be worth setting had to grant
Shell(...)— far more reach than searching needs.Set-ShpToolPolicy -Rule 'Read(./**)'is now enough to let the model locate a file by name or by
content whilerun_commandstays denied.Every returned hit is policy-checked, not just the search root, because a
glob rooted at an allowed directory can still match a path that resolves,
through a link, to somewhere no rule covers. An excluded hit is counted in
excludedByPolicyrather than dropped silently. Both results are bounded —
files examined, matches returned, and characters returned — and set
truncatedwhen any cap bites, so the model narrows the pattern instead of
overflowing the context window. -
ConvertTo-ShpAnnotationsurfaces structured findings in CI. Pipe a
ShellPilot.ResultfromInvoke-Shp -JsonSchema, or any plain finding
object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
task.logissuecommands, or readable text.Level,Path,Line,
Column,Title, andMessageare matched case-insensitively and can be
redirected with-PropertyMap; an unknown or missing level is always a
warning. Vendor-specific escaping keeps newlines and delimiters from
corrupting a workflow command. Output stays on the success stream unless
-Emitwrites it to the host, and-Summaryappe...
v0.4.0-preview0013
[v0.4.0-preview0013]
Security
-
Protect Unix
edit_filestaging from creation. Apply the source file
mode when creating the empty temporary file, before any content is copied,
so a private source is not temporarily exposed through default permissions. -
Keep
edit_fileon the target approved by the tool policy. Repointing
the original directory alias after authorization no longer redirects the
edit to a different file. Confirmation names the authorized target. Refuse
special files again after staging so a named-pipe swap cannot block the
final content check. External filesystem races remain outside the tool's
guarantees. -
Tool policy refuses paths when link resolution fails. A missing runtime
API or filesystem error no longer leaves an unresolved path eligible for
Read or Write access. This prevents a junction inside an allowed directory
from bypassing rules for its destination on an unsupported runtime. -
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
Add explicit estimated budgets for trusted owned requests. Opt into
RequestBudgetMode provider-estimatefor non-refundable reservations and
upward reconciliation; default verified counters keep their meaning.
Add the bounded Claude child provider/counting path, pre-generation host
admission, and secret-free reported/partial Usage. No automatic retries,
Model substitution, or invoice guarantee is implied. See
the transport contract. -
Add conditional admission limits to owned
Invoke-Shptransport.
RequestLimitsand a trustedRequestTokenCounterreserve input, maximum
output, and Engine-priced cost before dispatch. Unknown or failed Usage
retains its reservation and stays explicitly unknown in results, events,
and summaries. No verified Copilot counter or complete child runtime is
supplied; ordinary calls are unchanged. See
the contract and limitations. -
Add optional host transport groundwork to
Invoke-Shp. Use
NoAutomaticRetryto disable transport and API-shape resends, or
RequestTransportfor a credentialless Tool-calling loop with a trusted
caller-owned transport. Ordinary calls are unchanged. Hard request admission,
process isolation, and a trusted transport process are not supplied by these
options. See the contract and limitations. -
Add
edit_filefor targeted changes to existing files. Replace one
exact, case-sensitiveoldStringwithnewString, refusing zero matches or
multiple matches with recovery guidance. Preserve the BOM, encoding and
unchanged line endings for UTF-8 and BOM-marked UTF-16/UTF-32; refuse
malformed or unsupported text instead of converting it. The tool is disabled
by-DisableFileAccess, and reports the intended edit without writing under
-WhatIf.When a tool policy is set, an edit needs both a
Read()and aWrite()rule
covering the target, and a deny in either kind refuses it. Match counts
disclose file content even when the replacement is identical, soWrite()
alone would turn the tool into a way to confirm guesses about a file the
policy never granted read access to.write_fileandcreate_directoryare
unchanged and still need onlyWrite().Only regular, seekable files are eligible, and input and output are each
capped at 8 MiB including the BOM. Unix file types are checked directly,
avoiding a hang when formatted permissions misidentify a named pipe as a
regular file. The edit is staged in the target's own
directory, flushed, checked against the file's current bytes, and then put in
place with a backup of the original. A native replacement failure that moves
the original away retains the backup and reportsrecoveryPathfor manual
recovery instead of deleting the remaining copy. Cleanup is best effort.
Detected concurrent changes are refused; the final check and replacement
are not a compare-and-swap guarantee.
See README.md. -
glob_filesandgrep_fileslet the model search without a shell.
glob_filesfinds files by name pattern under a directory;grep_files
searches their contents and returns only the path, line number and matching
line, leavingread_fileto read around a hit. Both are offered with the
other file tools and withdrawn by-DisableFileAccess.Both are governed by the existing
Read()rules ofSet-ShpToolPolicy, and
that is the point: until now the only way to make the model find something
wasrun_command, so a policy tight enough to be worth setting had to grant
Shell(...)— far more reach than searching needs.Set-ShpToolPolicy -Rule 'Read(./**)'is now enough to let the model locate a file by name or by
content whilerun_commandstays denied.Every returned hit is policy-checked, not just the search root, because a
glob rooted at an allowed directory can still match a path that resolves,
through a link, to somewhere no rule covers. An excluded hit is counted in
excludedByPolicyrather than dropped silently. Both results are bounded —
files examined, matches returned, and characters returned — and set
truncatedwhen any cap bites, so the model narrows the pattern instead of
overflowing the context window. -
ConvertTo-ShpAnnotationsurfaces structured findings in CI. Pipe a
ShellPilot.ResultfromInvoke-Shp -JsonSchema, or any plain finding
object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
task.logissuecommands, or readable text.Level,Path,Line,
Column,Title, andMessageare matched case-insensitively and can be
redirected with-PropertyMap; an unknown or missing level is always a
warning. Vendor-specific escaping keeps newlines and delimiters from
corrupting a workflow command. Output stays on the success stream unless
-Emitwrites it to the host, and-Summaryappends a Markdown table to
$env:GITHUB_STEP_SUMMARYwhen available.
See specs/028-ci-annotations.md. -
Invoke-Shp -EventStream <path>writes a headless JSONL event stream. A
CI log collector reads lines, not prose: everything the module said about a
running turn was aimed at a person, so a nineteen-iteration turn that was
refused twice by the tool policy, retried once on an expired session token
and then stopped on-MaxBudgetUSDleft one object saying
BudgetExceeded = $trueand nothing about the shape of the failure. The
stream appends one JSON object per line -turn.start,model.request,
usage,reasoning(one per streamed chunk under-ShowThinking),
tool.call,tool.result,todo,retry,error,final- each carrying
schemaVersion, a monotonicsequence, an ISO 8601 UTCtimestamp, a
typeand a flatdataobject. Pass-to write the records to the
Information stream instead of a file. Every line is appended whole, so a run
killed mid-turn still leaves a file that parses up to its last complete line;
a later call appending to that valid stream continues the sequence. Every
string payload goes through the same redaction seam the request body does, so
a secret a tool printed does not reach the stream verbatim; arun_command
tool-call record names the tool and the policy decision but never the command
line. The complete streamed reasoning trace is redacted before it is divided
back into Event records, so an SSE boundary cannot split a secret around the
redaction seam; partial reasoning is retained before aretryorerror.
Transient HTTP and network-outage retries from the shared request wrapper are
recorded with attempt, delay and status data, and an inventedask_usercall
in a non-interactive turn records its denied Tool call and terminal error
before the call stops.-DisableProgressEventsno longer switches this off -
the two sinks are gated independently.
See specs/027-headless-event-stream.md. -
Invoke-Shp -AsJobandInvoke-ShpBatch -AsJobrun a call in the
background. Both return a thread job whoseReceive-Jobresolves to the
sameShellPilot.Result/ShellPilot.BatchResultobjects the synchronous
call returns - the same process, so nothing is serialised into a
`Deserialized...
v0.4.0-preview0012
[v0.4.0-preview0012]
Security
-
Protect Unix
edit_filestaging from creation. Apply the source file
mode when creating the empty temporary file, before any content is copied,
so a private source is not temporarily exposed through default permissions. -
Keep
edit_fileon the target approved by the tool policy. Repointing
the original directory alias after authorization no longer redirects the
edit to a different file. Confirmation names the authorized target. Refuse
special files again after staging so a named-pipe swap cannot block the
final content check. External filesystem races remain outside the tool's
guarantees. -
Tool policy refuses paths when link resolution fails. A missing runtime
API or filesystem error no longer leaves an unresolved path eligible for
Read or Write access. This prevents a junction inside an allowed directory
from bypassing rules for its destination on an unsupported runtime. -
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
Add
edit_filefor targeted changes to existing files. Replace one
exact, case-sensitiveoldStringwithnewString, refusing zero matches or
multiple matches with recovery guidance. Preserve the BOM, encoding and
unchanged line endings for UTF-8 and BOM-marked UTF-16/UTF-32; refuse
malformed or unsupported text instead of converting it. The tool is disabled
by-DisableFileAccess, and reports the intended edit without writing under
-WhatIf.When a tool policy is set, an edit needs both a
Read()and aWrite()rule
covering the target, and a deny in either kind refuses it. Match counts
disclose file content even when the replacement is identical, soWrite()
alone would turn the tool into a way to confirm guesses about a file the
policy never granted read access to.write_fileandcreate_directoryare
unchanged and still need onlyWrite().Only regular, seekable files are eligible, and input and output are each
capped at 8 MiB including the BOM. Unix file types are checked directly,
avoiding a hang when formatted permissions misidentify a named pipe as a
regular file. The edit is staged in the target's own
directory, flushed, checked against the file's current bytes, and then put in
place with a backup of the original. A native replacement failure that moves
the original away retains the backup and reportsrecoveryPathfor manual
recovery instead of deleting the remaining copy. Cleanup is best effort.
Detected concurrent changes are refused; the final check and replacement
are not a compare-and-swap guarantee.
See README.md. -
glob_filesandgrep_fileslet the model search without a shell.
glob_filesfinds files by name pattern under a directory;grep_files
searches their contents and returns only the path, line number and matching
line, leavingread_fileto read around a hit. Both are offered with the
other file tools and withdrawn by-DisableFileAccess.Both are governed by the existing
Read()rules ofSet-ShpToolPolicy, and
that is the point: until now the only way to make the model find something
wasrun_command, so a policy tight enough to be worth setting had to grant
Shell(...)— far more reach than searching needs.Set-ShpToolPolicy -Rule 'Read(./**)'is now enough to let the model locate a file by name or by
content whilerun_commandstays denied.Every returned hit is policy-checked, not just the search root, because a
glob rooted at an allowed directory can still match a path that resolves,
through a link, to somewhere no rule covers. An excluded hit is counted in
excludedByPolicyrather than dropped silently. Both results are bounded —
files examined, matches returned, and characters returned — and set
truncatedwhen any cap bites, so the model narrows the pattern instead of
overflowing the context window. -
ConvertTo-ShpAnnotationsurfaces structured findings in CI. Pipe a
ShellPilot.ResultfromInvoke-Shp -JsonSchema, or any plain finding
object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
task.logissuecommands, or readable text.Level,Path,Line,
Column,Title, andMessageare matched case-insensitively and can be
redirected with-PropertyMap; an unknown or missing level is always a
warning. Vendor-specific escaping keeps newlines and delimiters from
corrupting a workflow command. Output stays on the success stream unless
-Emitwrites it to the host, and-Summaryappends a Markdown table to
$env:GITHUB_STEP_SUMMARYwhen available.
See specs/028-ci-annotations.md. -
Invoke-Shp -EventStream <path>writes a headless JSONL event stream. A
CI log collector reads lines, not prose: everything the module said about a
running turn was aimed at a person, so a nineteen-iteration turn that was
refused twice by the tool policy, retried once on an expired session token
and then stopped on-MaxBudgetUSDleft one object saying
BudgetExceeded = $trueand nothing about the shape of the failure. The
stream appends one JSON object per line -turn.start,model.request,
usage,reasoning(one per streamed chunk under-ShowThinking),
tool.call,tool.result,todo,retry,error,final- each carrying
schemaVersion, a monotonicsequence, an ISO 8601 UTCtimestamp, a
typeand a flatdataobject. Pass-to write the records to the
Information stream instead of a file. Every line is appended whole, so a run
killed mid-turn still leaves a file that parses up to its last complete line;
a later call appending to that valid stream continues the sequence. Every
string payload goes through the same redaction seam the request body does, so
a secret a tool printed does not reach the stream verbatim; arun_command
tool-call record names the tool and the policy decision but never the command
line. The complete streamed reasoning trace is redacted before it is divided
back into Event records, so an SSE boundary cannot split a secret around the
redaction seam; partial reasoning is retained before aretryorerror.
Transient HTTP and network-outage retries from the shared request wrapper are
recorded with attempt, delay and status data, and an inventedask_usercall
in a non-interactive turn records its denied Tool call and terminal error
before the call stops.-DisableProgressEventsno longer switches this off -
the two sinks are gated independently.
See specs/027-headless-event-stream.md. -
Invoke-Shp -AsJobandInvoke-ShpBatch -AsJobrun a call in the
background. Both return a thread job whoseReceive-Jobresolves to the
sameShellPilot.Result/ShellPilot.BatchResultobjects the synchronous
call returns - the same process, so nothing is serialised into a
Deserialized.*copy. The job runspace inherits no module state, so the
session context, session defaults, cached model limits, tool policy,
redaction policy and registered tools are replayed into it and the module is
imported by path.Invoke-Shp -AsJobis seeded from a snapshot of the
session conversation and stays stateless from there, because a job that
finishes at an arbitrary time must not race the caller's next call. The CI
entitlement gate is still evaluated at the call site, so a refused backend
fails where you typed it rather than in the background of a green build. An
event stream is honoured:-AsJobdoes not silently turn it off.
See specs/027-headless-event-stream.md. -
Invoke-Shpnow redacts secrets before they leave the runner. A CI job
feeds the model diffs, build logs and attachments produced by untrusted
pull-request content, and nothing scrubbed them before now - a leaked token
in a log became a token sent to a third party. Immediately before each
round-trip, the prompt, every inlined-Attachment, and every tool result
(run_command,read_file,fetch_url, an MCP tool, a user-defined tool)
is scanned for six built-in shapes - GitHub tokens, AWS access key ids, PEM
private-key blocks, JWTs, basic-auth URL credentials, and connection-string
password fields - and a match is replaced with a stable, named placeholder
such as ...
v0.4.0-preview0011
[v0.4.0-preview0011]
Security
-
A disabled tool can no longer be executed.
-DisableTerminal,
-DisableFileAccess,-DisableBrowsing,-DisableUserPromptsand
-DisableTodoListremoved a tool from the set offered to the model, but the
dispatch switch matched built-in tool names unconditionally — so a model that
named a disabled tool anyway, from its own priors or from a replayed history,
had it run.-DisableTerminalbounded what was advertised and nothing about
what executed.Dispatch now refuses any built-in that this call did not offer, before the
tool runs. The refusal reuses the existing tool-policy path: thetool.call
event carriespolicy = denied, the reason names the disabled tool, the call
appears onToolCallsDenied, and the model receives{"denied": "..."}so it
can choose another route instead of failing the turn. The offered set is
derived from the assembled tool list rather than re-tested against each
switch, so a tool added later cannot be offered under one condition and
dispatched under another. -
Register-ShpToolrefuses a built-in tool name. Dispatch matches built-in
names before it consults the user tool table, so registeringrun_command,
read_fileor any other built-in produced a tool that was advertised to the
model and then silently ignored while the built-in ran instead — with the
caller believing it had replaced it. An attached MCP server has always been
refused a colliding name; a local registration now fails the same way, loudly
and at registration time. Choose a distinct-ToolName.
Added
-
glob_filesandgrep_fileslet the model search without a shell.
glob_filesfinds files by name pattern under a directory;grep_files
searches their contents and returns only the path, line number and matching
line, leavingread_fileto read around a hit. Both are offered with the
other file tools and withdrawn by-DisableFileAccess.Both are governed by the existing
Read()rules ofSet-ShpToolPolicy, and
that is the point: until now the only way to make the model find something
wasrun_command, so a policy tight enough to be worth setting had to grant
Shell(...)— far more reach than searching needs.Set-ShpToolPolicy -Rule 'Read(./**)'is now enough to let the model locate a file by name or by
content whilerun_commandstays denied.Every returned hit is policy-checked, not just the search root, because a
glob rooted at an allowed directory can still match a path that resolves,
through a link, to somewhere no rule covers. An excluded hit is counted in
excludedByPolicyrather than dropped silently. Both results are bounded —
files examined, matches returned, and characters returned — and set
truncatedwhen any cap bites, so the model narrows the pattern instead of
overflowing the context window. -
ConvertTo-ShpAnnotationsurfaces structured findings in CI. Pipe a
ShellPilot.ResultfromInvoke-Shp -JsonSchema, or any plain finding
object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
task.logissuecommands, or readable text.Level,Path,Line,
Column,Title, andMessageare matched case-insensitively and can be
redirected with-PropertyMap; an unknown or missing level is always a
warning. Vendor-specific escaping keeps newlines and delimiters from
corrupting a workflow command. Output stays on the success stream unless
-Emitwrites it to the host, and-Summaryappends a Markdown table to
$env:GITHUB_STEP_SUMMARYwhen available.
See specs/028-ci-annotations.md. -
Invoke-Shp -EventStream <path>writes a headless JSONL event stream. A
CI log collector reads lines, not prose: everything the module said about a
running turn was aimed at a person, so a nineteen-iteration turn that was
refused twice by the tool policy, retried once on an expired session token
and then stopped on-MaxBudgetUSDleft one object saying
BudgetExceeded = $trueand nothing about the shape of the failure. The
stream appends one JSON object per line -turn.start,model.request,
usage,reasoning(one per streamed chunk under-ShowThinking),
tool.call,tool.result,todo,retry,error,final- each carrying
schemaVersion, a monotonicsequence, an ISO 8601 UTCtimestamp, a
typeand a flatdataobject. Pass-to write the records to the
Information stream instead of a file. Every line is appended whole, so a run
killed mid-turn still leaves a file that parses up to its last complete line;
a later call appending to that valid stream continues the sequence. Every
string payload goes through the same redaction seam the request body does, so
a secret a tool printed does not reach the stream verbatim; arun_command
tool-call record names the tool and the policy decision but never the command
line. The complete streamed reasoning trace is redacted before it is divided
back into Event records, so an SSE boundary cannot split a secret around the
redaction seam; partial reasoning is retained before aretryorerror.
Transient HTTP and network-outage retries from the shared request wrapper are
recorded with attempt, delay and status data, and an inventedask_usercall
in a non-interactive turn records its denied Tool call and terminal error
before the call stops.-DisableProgressEventsno longer switches this off -
the two sinks are gated independently.
See specs/027-headless-event-stream.md. -
Invoke-Shp -AsJobandInvoke-ShpBatch -AsJobrun a call in the
background. Both return a thread job whoseReceive-Jobresolves to the
sameShellPilot.Result/ShellPilot.BatchResultobjects the synchronous
call returns - the same process, so nothing is serialised into a
Deserialized.*copy. The job runspace inherits no module state, so the
session context, session defaults, cached model limits, tool policy,
redaction policy and registered tools are replayed into it and the module is
imported by path.Invoke-Shp -AsJobis seeded from a snapshot of the
session conversation and stays stateless from there, because a job that
finishes at an arbitrary time must not race the caller's next call. The CI
entitlement gate is still evaluated at the call site, so a refused backend
fails where you typed it rather than in the background of a green build. An
event stream is honoured:-AsJobdoes not silently turn it off.
See specs/027-headless-event-stream.md. -
Invoke-Shpnow redacts secrets before they leave the runner. A CI job
feeds the model diffs, build logs and attachments produced by untrusted
pull-request content, and nothing scrubbed them before now - a leaked token
in a log became a token sent to a third party. Immediately before each
round-trip, the prompt, every inlined-Attachment, and every tool result
(run_command,read_file,fetch_url, an MCP tool, a user-defined tool)
is scanned for six built-in shapes - GitHub tokens, AWS access key ids, PEM
private-key blocks, JWTs, basic-auth URL credentials, and connection-string
password fields - and a match is replaced with a stable, named placeholder
such as[redacted:github-token], never simply deleted. The result reports
Redactions: pattern name and count only, never the matched value.
Set-ShpRedactionPolicy/Get-ShpRedactionPolicy/Clear-ShpRedactionPolicy
add custom patterns on top of the built-ins, in the sameName(Pattern)
shapeSet-ShpToolPolicyalready uses, and the custom policy travels to
everyInvoke-ShpBatchworker the same way the tool policy does. Redaction
is on by default; pass-DisableRedactionto send a call verbatim. Only the
model's own reply is exempt - it was generated from input already redacted
before it was sent, so it cannot reflect a secret it was never shown, and a
-JsonSchemareply still parses ontoContentObjectexactly as it would
with redaction off.
See specs/026-egress-redaction.md. -
An unattended run is now a supported, deliberate profile rather than an
accident.Invoke-Shp,Invoke-ShpBatchandInitialize-Shptake
-NonInteractive, on automatically when$env:CIis truthy and overridable
with-NonInteractive:$false. It withdrawsask_user, refuses-Confirm
instead of silently answering it yes, and refuses the device-code flow before
the browser launch and the clipboard write - because a prompt on a runner does
not fail, it burns the job's whole timeout and then fails for the wrong
reason. A model that callsask_useranyway ends the turn with
ShpNonInteractivePromptrather than continuing on an answer nobody gave.
See specs/025-ci-profile.md. -
In CI, the default Copilot backend is refused unless you opt in. That
backend reaches the Copilot endpoints with the public VS Code client id, on
the token owner's personal entitlement - fine for a shell, a decision for a
pipeline. Configure an OpenAI-compatible endpoint instead, or set
SHELLPILOT_ALLOW_COPILOT_BACKEND_IN_CI. The error carries the id
ShpCopilotBackendInCiand names both remedies, and it is raised before
the token exchange so nothing is spent proving the point.
A warning was the obvious alternative and is the wrong shape: nobody reads a
warning in a green build, which is the whole finding behind-FailOn. -
$env:SHELLPILOT_API_BASEand$env:SHELLPILOT_API_KEYare read as
backend defaults, below an explicit-ApiBaseand the session context and
above the built-in Copilot endpoint - so a pipeline points ShellPilot at its
own endpoint with the variables it already injects. -
Test-ShpCiReadinessreports the whole res...
v0.4.0-preview0010
[v0.4.0-preview0010]
Added
-
ConvertTo-ShpAnnotationsurfaces structured findings in CI. Pipe a
ShellPilot.ResultfromInvoke-Shp -JsonSchema, or any plain finding
object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
task.logissuecommands, or readable text.Level,Path,Line,
Column,Title, andMessageare matched case-insensitively and can be
redirected with-PropertyMap; an unknown or missing level is always a
warning. Vendor-specific escaping keeps newlines and delimiters from
corrupting a workflow command. Output stays on the success stream unless
-Emitwrites it to the host, and-Summaryappends a Markdown table to
$env:GITHUB_STEP_SUMMARYwhen available.
See specs/028-ci-annotations.md. -
Invoke-Shp -EventStream <path>writes a headless JSONL event stream. A
CI log collector reads lines, not prose: everything the module said about a
running turn was aimed at a person, so a nineteen-iteration turn that was
refused twice by the tool policy, retried once on an expired session token
and then stopped on-MaxBudgetUSDleft one object saying
BudgetExceeded = $trueand nothing about the shape of the failure. The
stream appends one JSON object per line -turn.start,model.request,
usage,reasoning(one per streamed chunk under-ShowThinking),
tool.call,tool.result,todo,retry,error,final- each carrying
schemaVersion, a monotonicsequence, an ISO 8601 UTCtimestamp, a
typeand a flatdataobject. Pass-to write the records to the
Information stream instead of a file. Every line is appended whole, so a run
killed mid-turn still leaves a file that parses up to its last complete line;
a later call appending to that valid stream continues the sequence. Every
string payload goes through the same redaction seam the request body does, so
a secret a tool printed does not reach the stream verbatim; arun_command
tool-call record names the tool and the policy decision but never the command
line. The complete streamed reasoning trace is redacted before it is divided
back into Event records, so an SSE boundary cannot split a secret around the
redaction seam; partial reasoning is retained before aretryorerror.
Transient HTTP and network-outage retries from the shared request wrapper are
recorded with attempt, delay and status data, and an inventedask_usercall
in a non-interactive turn records its denied Tool call and terminal error
before the call stops.-DisableProgressEventsno longer switches this off -
the two sinks are gated independently.
See specs/027-headless-event-stream.md. -
Invoke-Shp -AsJobandInvoke-ShpBatch -AsJobrun a call in the
background. Both return a thread job whoseReceive-Jobresolves to the
sameShellPilot.Result/ShellPilot.BatchResultobjects the synchronous
call returns - the same process, so nothing is serialised into a
Deserialized.*copy. The job runspace inherits no module state, so the
session context, session defaults, cached model limits, tool policy,
redaction policy and registered tools are replayed into it and the module is
imported by path.Invoke-Shp -AsJobis seeded from a snapshot of the
session conversation and stays stateless from there, because a job that
finishes at an arbitrary time must not race the caller's next call. The CI
entitlement gate is still evaluated at the call site, so a refused backend
fails where you typed it rather than in the background of a green build. An
event stream is honoured:-AsJobdoes not silently turn it off.
See specs/027-headless-event-stream.md. -
Invoke-Shpnow redacts secrets before they leave the runner. A CI job
feeds the model diffs, build logs and attachments produced by untrusted
pull-request content, and nothing scrubbed them before now - a leaked token
in a log became a token sent to a third party. Immediately before each
round-trip, the prompt, every inlined-Attachment, and every tool result
(run_command,read_file,fetch_url, an MCP tool, a user-defined tool)
is scanned for six built-in shapes - GitHub tokens, AWS access key ids, PEM
private-key blocks, JWTs, basic-auth URL credentials, and connection-string
password fields - and a match is replaced with a stable, named placeholder
such as[redacted:github-token], never simply deleted. The result reports
Redactions: pattern name and count only, never the matched value.
Set-ShpRedactionPolicy/Get-ShpRedactionPolicy/Clear-ShpRedactionPolicy
add custom patterns on top of the built-ins, in the sameName(Pattern)
shapeSet-ShpToolPolicyalready uses, and the custom policy travels to
everyInvoke-ShpBatchworker the same way the tool policy does. Redaction
is on by default; pass-DisableRedactionto send a call verbatim. Only the
model's own reply is exempt - it was generated from input already redacted
before it was sent, so it cannot reflect a secret it was never shown, and a
-JsonSchemareply still parses ontoContentObjectexactly as it would
with redaction off.
See specs/026-egress-redaction.md. -
An unattended run is now a supported, deliberate profile rather than an
accident.Invoke-Shp,Invoke-ShpBatchandInitialize-Shptake
-NonInteractive, on automatically when$env:CIis truthy and overridable
with-NonInteractive:$false. It withdrawsask_user, refuses-Confirm
instead of silently answering it yes, and refuses the device-code flow before
the browser launch and the clipboard write - because a prompt on a runner does
not fail, it burns the job's whole timeout and then fails for the wrong
reason. A model that callsask_useranyway ends the turn with
ShpNonInteractivePromptrather than continuing on an answer nobody gave.
See specs/025-ci-profile.md. -
In CI, the default Copilot backend is refused unless you opt in. That
backend reaches the Copilot endpoints with the public VS Code client id, on
the token owner's personal entitlement - fine for a shell, a decision for a
pipeline. Configure an OpenAI-compatible endpoint instead, or set
SHELLPILOT_ALLOW_COPILOT_BACKEND_IN_CI. The error carries the id
ShpCopilotBackendInCiand names both remedies, and it is raised before
the token exchange so nothing is spent proving the point.
A warning was the obvious alternative and is the wrong shape: nobody reads a
warning in a green build, which is the whole finding behind-FailOn. -
$env:SHELLPILOT_API_BASEand$env:SHELLPILOT_API_KEYare read as
backend defaults, below an explicit-ApiBaseand the session context and
above the built-in Copilot endpoint - so a pipeline points ShellPilot at its
own endpoint with the variables it already injects. -
Test-ShpCiReadinessreports the whole resolved profile - token source,
backend, interactive capability,Ready, and a list of named issues - without
sending a chat request or exchanging a token. The three things an unattended
run needs are decided by three different precedence chains with silent
fallbacks, so a misconfigured job otherwise fails minutes later at the first
Invoke-Shpwith whichever chain gave out first. No secret is returned: the
credential and the API key are reported by source only, and the endpoint has
any URL credentials redacted.
Changed
-
An alternative backend (
-ApiBase) no longer carries the Copilot session
token. It previously fell back to that token whenever noApiKeywas
configured. That was reachable before only throughSet-ShpContext; reading
ApiBasefrom the environment would have let anything able to set a variable
on a runner redirect a live Copilot credential to a host of its choosing.
With a key the request carries the key; without one it carries no
Authorizationheader at all, which is what a local server expects anyway.
A URL's credentials are also redacted from the result'sEndpointmember. -
A pipeline step can now fail when the call did not deliver. A budget
overrun was aWrite-Warningplus aBudgetExceededproperty, so an
unattended run exited0on a truncated or abandoned answer and wrote the
half-finished artifact anyway.Invoke-Shp -FailOnturns five named outcomes
into terminating errors:BudgetExceeded,Truncated,ToolIterationLimit,
NoContentandSchemaMismatch.
Each carries a distinct, documentedFullyQualifiedErrorId
(ShpBudgetExceeded,Invoke-Shpand so on) so a wrapper branches on the
condition instead of matching an English message - which is what
MaxToolIterationsforced, having used its own message text as the error id.
Omitting-FailOnchanges nothing, and the turn's side effects are
unchanged either way: the call is evaluated last, after the result is built,
the usage row written and the session chat updated, so-FailOndecides only
whether the call ends with a result or with an error. The whole
ShellPilot.Resultrides onErrorRecord.TargetObject, so acatchblock
still knows what the abandoned turn cost.
ShellPilot never sets$LASTEXITCODEand never callsexit- a module that
terminates its host cannot be composed - so the exit code stays the caller's
job; the comment-based help carries thetry/catchplusexit 1wrapper.
See specs/024-pipeline-failure-semantics.md. -
Invoke-ShpBatch -FailOnand-FailBatchOnAnyItem. The same five
conditions apply per item, and a tripped one never aborts the batch: the item
reportsSuccess = $falsewith the branchableErrorRecordintact while every
other item runs to completion. `...
v0.4.0-preview0009
[v0.4.0-preview0009]
Added
-
Invoke-Shp -Attachmenttakes a file of any format.-Imagewas the only
attachment surface and accepts images only, so everything else had no route to
the model at all - naming the path in the prompt works for a text file and
fails for anything else, becauseread_filedecodes text and a.msgcomes
back as 2,257 characters of raw bytes.
Each file is classified by content, not extension, and routed three ways:
an image joins the vision path; a text file of any encoding is decoded and
inlined, capped with the usual truncation marker; and a binary file is
described rather than decoded - the model is given the absolute path, the
size, the format identified from the file's magic number, and a hex preview of
the head.
ShellPilot deliberately converts nothing. A converter table would need a
dependency and a new extractor per format, against a module whose stated
constraint is no runtime dependencies - while the model already hasread_file
andrun_commandand lacked only the first bytes. Live-verified on the.msg
that motivated this, with neither Outlook nor Python present: given
d0 cf 11 e0 a1 b1 1a e1the model recognised the OLE2 compound file and
decoded the sender, subject, timestamp, flight number and booking code itself
in four iterations.
Attachment content is inserted into the user message and framed as data,
never as a system instruction, because a document is untrusted content
(spec 019's threat model). The payload is kept out of the replayed history, so
a continuation records that files were attached without resending them. The
result reportsAttachments.
See specs/022-file-attachments.md. -
Register-ShpMcpServer,Get-ShpMcpServerandUnregister-ShpMcpServer
attach MCP (Model Context Protocol) servers so their tools are offered to the
model alongside the built-ins and any registered user tools. Opt out for one
call withInvoke-Shp -DisableMcp; nothing is offered until you attach a
server, so the default posture is unchanged.
Both protocol eras are supported over stdio. Verified from the
specification rather than from memory, which changed the design: the current
revision 2026-07-28 removed theinitializehandshake entirely - a modern
request is stateless and carries its protocol version and client capabilities
in_meta, withserver/discoveras a mandatory RPC - while nearly every
server in the field still expects the handshake. The client therefore probes
withserver/discoverand falls back toinitializeon any other error or a
timeout, never keyed to one error code. Both eras were negotiated live, and
the real Azure MCP Server 2.0.5 turned out to be a legacy server - which
settles whether supporting both was worth it.
See specs/021-mcp-server-support.md. -
Tool names are namespaced
mcp_<alias>_<tool>using the alias you chose,
not the server's self-reported name, which the protocol says nothing
verifies. The sanitiser is written to a measured constraint, not a guessed
one: the Copilot endpoint enforces^[a-zA-Z0-9_-]{1,128}$, so a dot - which
MCP explicitly permits in a tool name - becomes_. This matters because a
rejected name identifies the offending tool only by its index in the
request, and the resulting 400 is masked by the chat-to-responses fallback,
surfacing as "model ... does not support Responses API" - a true statement
about an entirely different problem. -
Get-ShpToolnow lists MCP tools next to user tools, with a newOrigin
column (UserorMcp) and the contributingServer.Invoke-Shpreports
McpEnabled,McpServersAvailable,McpToolsAvailableandMcpToolsCalled,
and an MCP call emits the sameToolCallprogress record as every other tool,
so a host renders it identically.
Security
- An MCP server is a third-party process running with your privileges, and
there is no sandbox. Its tool names and descriptions are untrusted input the
model reads on every round-trip, and its results are untrusted content. Three
controls follow, each stated with its limits:- Nothing is discovered. No scan of the working directory,
.vscodeor a
user profile.Register-ShpMcpServer -Pathreads a file you name. A
configuration file is a command line, and a command line is arbitrary code. - The tool list is frozen at registration. The client opens no
subscriptions/listenstream, so it receives no
notifications/tools/list_changedand a server cannot add or alter tools
after you approved them. Refreshing is an explicit-Force. This does not
stop a server changing its behaviour. - The child environment is built, not inherited. Unlike
run_command,
which deliberately inherits the whole block for compatibility, an MCP child
starts from a minimal base plus exactly the variables you name in
-Environment, so an ambient$env:credential is not handed to somebody
else's process.
- Nothing is discovered. No scan of the working directory,
Set-ShpToolPolicycannot gate an MCP call, and this is stated rather than
implied. Its rules match resolved filesystem paths and leading command
tokens; a tool call has neither. Demonstrated in one live Turn under
Read(<repo>/**): the built-inread_filewas denied with a reason and the
MCP tool call ran. A policy that scopesread_fileto one directory does
nothing about an attached filesystem server. Reduce reach at attachment
instead, with-ToolName.- The injection path was measured, not asserted. With a hostile instruction in a
tool description only, the model read a decoy credentials file and passed
its contents to the third-party server as a tool argument - the server's own
log confirms receipt. The same run with-DisableFileAccessread nothing and
leaked nothing. - A configuration entry asking for
sandboxEnabledwarns and still starts:
a configuration written for a sandboxing host is exactly the one you want to
reuse. The gap is surfaced twice rather than being fatal once - a warning that
says the server is starting unsandboxed, plusSandboxRequestedon the server
record, because a warning scrolls away and a property does not. An entry
carrying an unresolved${...}variable is refused, because starting the
literal text would run a different command than the file describes. Get-ShpMcpServerreports environment variable names only, never values.
Known limits
-
stdio transport only. Streamable HTTP is deferred with the MCP Authorization
framework; a half-authorised HTTP client would be worse than none. -
Invoke-ShpBatchdoes not use attached servers and warns once. A worker
runspace inherits nothing, so replaying an attachment would start one copy of
every server per worker. -
A server that exits is marked
Faultedrather than restarted automatically.
The specification says a client SHOULD restart one; automatic respawn of
third-party code inside an unattended loop turns one crash into a crash loop
nobody is watching. Re-attach withRegister-ShpMcpServer -Force. -
Resources, prompts, sampling, elicitation and roots are out of scope for this
version. Client capabilities are declared empty, so a modern server needing
one gets a protocol error naming it rather than hanging. -
The cached OAuth token is now protected at rest, closing open decision #5.
On Windows it is DPAPI-encrypted for your account; on every platform the file
is restricted to the current user. Measured before the change, the real file
was 40 bytes of clear text with the profile's inherited ACL, readable by
BUILTIN\Administratorsas well as by you.
The file is self-describing -SHPv1:DPAPI:...orSHPv1:NONE:...- and
Initialize-Shpreports which protection it applied, because a scheme that
silently degrades to clear text is worse than clear text. On Linux and macOS
there is no DPAPI equivalent without a dependency, so the scheme isNONE
and file permissions (mode 600) are the only control; that is stated in the
file and on screen rather than implied.
No runtime dependency was added -ConvertTo-SecureStringis built in.
SecretManagement was considered and rejected: SecretStore prompts to unlock,
which would break every unattended run, and configuring it not to prompt
reduces it to file permissions while costing this module its empty dependency
list. This buys protection against another principal on the machine; it does
not protect against code running as you, and no candidate scheme would.
See specs/020-encrypted-token-storage.md. -
An existing clear-text token file keeps working and is upgraded in place by
Initialize-Shpwithout re-authenticating, so nobody needs a browser just
to gain protection. A protected file that cannot be decrypted - typically one
copied from another machine or account - throws with an actionable message
namingInitialize-Shp -Force, rather than sending a ciphertext to the
service as a bearer token. -
Set-ShpToolPolicy,Get-ShpToolPolicyandClear-ShpToolPolicyscope what
the unsandboxed file and shell tools may reach, so an unattended run can be
given the access it needs instead of the caller's entire filesystem and shell.
Rules are writtenRead(./src/**),Write(./out/**),Shell(git status),
with a leading!to deny; any matching deny beats every matching allow.
Nothing changes until you set a policy - and once you do, the model is
denied by default. Refusals are reported on the result asToolCallsDenied,
and the policy travels into everyInvoke-ShpBatchworker, because a worker
inherits no module state and would otherwise be the one unguarded path.
This closes a real gap rather than a symmetry:-Confirmis interactive...
v0.4.0-preview0008
[v0.4.0-preview0008]
Added
Register-ShpMcpServer,Get-ShpMcpServerandUnregister-ShpMcpServer
attach MCP (Model Context Protocol) servers so their tools are offered to the
model alongside the built-ins and any registered user tools. Opt out for one
call withInvoke-Shp -DisableMcp; nothing is offered until you attach a
server, so the default posture is unchanged.
Both protocol eras are supported over stdio. Verified from the
specification rather than from memory, which changed the design: the current
revision 2026-07-28 removed theinitializehandshake entirely - a modern
request is stateless and carries its protocol version and client capabilities
in_meta, withserver/discoveras a mandatory RPC - while nearly every
server in the field still expects the handshake. The client therefore probes
withserver/discoverand falls back toinitializeon any other error or a
timeout, never keyed to one error code. Both eras were negotiated live, and
the real Azure MCP Server 2.0.5 turned out to be a legacy server - which
settles whether supporting both was worth it.
See specs/021-mcp-server-support.md.- Tool names are namespaced
mcp_<alias>_<tool>using the alias you chose,
not the server's self-reported name, which the protocol says nothing
verifies. The sanitiser is written to a measured constraint, not a guessed
one: the Copilot endpoint enforces^[a-zA-Z0-9_-]{1,128}$, so a dot - which
MCP explicitly permits in a tool name - becomes_. This matters because a
rejected name identifies the offending tool only by its index in the
request, and the resulting 400 is masked by the chat-to-responses fallback,
surfacing as "model ... does not support Responses API" - a true statement
about an entirely different problem. Get-ShpToolnow lists MCP tools next to user tools, with a newOrigin
column (UserorMcp) and the contributingServer.Invoke-Shpreports
McpEnabled,McpServersAvailable,McpToolsAvailableandMcpToolsCalled,
and an MCP call emits the sameToolCallprogress record as every other tool,
so a host renders it identically.
Security
- An MCP server is a third-party process running with your privileges, and
there is no sandbox. Its tool names and descriptions are untrusted input the
model reads on every round-trip, and its results are untrusted content. Three
controls follow, each stated with its limits:- Nothing is discovered. No scan of the working directory,
.vscodeor a
user profile.Register-ShpMcpServer -Pathreads a file you name. A
configuration file is a command line, and a command line is arbitrary code. - The tool list is frozen at registration. The client opens no
subscriptions/listenstream, so it receives no
notifications/tools/list_changedand a server cannot add or alter tools
after you approved them. Refreshing is an explicit-Force. This does not
stop a server changing its behaviour. - The child environment is built, not inherited. Unlike
run_command,
which deliberately inherits the whole block for compatibility, an MCP child
starts from a minimal base plus exactly the variables you name in
-Environment, so an ambient$env:credential is not handed to somebody
else's process.
- Nothing is discovered. No scan of the working directory,
Set-ShpToolPolicycannot gate an MCP call, and this is stated rather than
implied. Its rules match resolved filesystem paths and leading command
tokens; a tool call has neither. Demonstrated in one live Turn under
Read(<repo>/**): the built-inread_filewas denied with a reason and the
MCP tool call ran. A policy that scopesread_fileto one directory does
nothing about an attached filesystem server. Reduce reach at attachment
instead, with-ToolName.- The injection path was measured, not asserted. With a hostile instruction in a
tool description only, the model read a decoy credentials file and passed
its contents to the third-party server as a tool argument - the server's own
log confirms receipt. The same run with-DisableFileAccessread nothing and
leaked nothing. - A configuration entry asking for
sandboxEnabledwarns and still starts:
a configuration written for a sandboxing host is exactly the one you want to
reuse. The gap is surfaced twice rather than being fatal once - a warning that
says the server is starting unsandboxed, plusSandboxRequestedon the server
record, because a warning scrolls away and a property does not. An entry
carrying an unresolved${...}variable is refused, because starting the
literal text would run a different command than the file describes. Get-ShpMcpServerreports environment variable names only, never values.
Known limits
-
stdio transport only. Streamable HTTP is deferred with the MCP Authorization
framework; a half-authorised HTTP client would be worse than none. -
Invoke-ShpBatchdoes not use attached servers and warns once. A worker
runspace inherits nothing, so replaying an attachment would start one copy of
every server per worker. -
A server that exits is marked
Faultedrather than restarted automatically.
The specification says a client SHOULD restart one; automatic respawn of
third-party code inside an unattended loop turns one crash into a crash loop
nobody is watching. Re-attach withRegister-ShpMcpServer -Force. -
Resources, prompts, sampling, elicitation and roots are out of scope for this
version. Client capabilities are declared empty, so a modern server needing
one gets a protocol error naming it rather than hanging. -
The cached OAuth token is now protected at rest, closing open decision #5.
On Windows it is DPAPI-encrypted for your account; on every platform the file
is restricted to the current user. Measured before the change, the real file
was 40 bytes of clear text with the profile's inherited ACL, readable by
BUILTIN\Administratorsas well as by you.
The file is self-describing -SHPv1:DPAPI:...orSHPv1:NONE:...- and
Initialize-Shpreports which protection it applied, because a scheme that
silently degrades to clear text is worse than clear text. On Linux and macOS
there is no DPAPI equivalent without a dependency, so the scheme isNONE
and file permissions (mode 600) are the only control; that is stated in the
file and on screen rather than implied.
No runtime dependency was added -ConvertTo-SecureStringis built in.
SecretManagement was considered and rejected: SecretStore prompts to unlock,
which would break every unattended run, and configuring it not to prompt
reduces it to file permissions while costing this module its empty dependency
list. This buys protection against another principal on the machine; it does
not protect against code running as you, and no candidate scheme would.
See specs/020-encrypted-token-storage.md. -
An existing clear-text token file keeps working and is upgraded in place by
Initialize-Shpwithout re-authenticating, so nobody needs a browser just
to gain protection. A protected file that cannot be decrypted - typically one
copied from another machine or account - throws with an actionable message
namingInitialize-Shp -Force, rather than sending a ciphertext to the
service as a bearer token. -
Set-ShpToolPolicy,Get-ShpToolPolicyandClear-ShpToolPolicyscope what
the unsandboxed file and shell tools may reach, so an unattended run can be
given the access it needs instead of the caller's entire filesystem and shell.
Rules are writtenRead(./src/**),Write(./out/**),Shell(git status),
with a leading!to deny; any matching deny beats every matching allow.
Nothing changes until you set a policy - and once you do, the model is
denied by default. Refusals are reported on the result asToolCallsDenied,
and the policy travels into everyInvoke-ShpBatchworker, because a worker
inherits no module state and would otherwise be the one unguarded path.
This closes a real gap rather than a symmetry:-Confirmis interactive only
so an unattended run never prompts, and-DisableFileAccess/
-DisableTerminalare all-or-nothing, so the only safe unattended setting was
"all tools off".
See specs/019-tool-access-policy.md. -
Paths are matched on the absolute, link-resolved path rather than the
string the model supplied, so neither a..segment nor a directory junction
walks out of an allowed root, and patterns are anchored so a rule forout
cannot matchoutsider.run_commandis matched on whole leading tokens and
refuses any shell metacharacter whatever the rules say, because
git status; curl ...would otherwise pass a rule that only ever meant
git status. AShellrule constrains which program runs, not what it does;
the spec states that limit and the others plainly. -
Invoke-Shp -RetryDelaySec, and-TimeoutSec,-MaxRetryCount,
-RetryDelaySecand-NetworkOutageToleranceSeconGet-ShpModeland
Request-ShpEmbedding. All four now resolve identically everywhere - explicit
parameter, thenSet-ShpContext, then the built-in default - and apply to
every request the module makes, including the OAuth-to-session token
exchange,/modelsand embeddings. Those three previously called the retry
wrapper with the module's own defaults, soSet-ShpContext -TimeoutSec 10 -MaxRetryCount 0silently did not apply to them; andInvoke-Shpresolved
its options after the token exchange had already run, so an explicit
-TimeoutSecnever reached the one request that gates every other one. There
is deliberately no exemption for the auth handshake: the exchange is cached,
so-MaxRetryCount 0costs at most one un-retried attempt per ...
v0.4.0-preview0007
[v0.4.0-preview0007]
Added
Register-ShpMcpServer,Get-ShpMcpServerandUnregister-ShpMcpServer
attach MCP (Model Context Protocol) servers so their tools are offered to the
model alongside the built-ins and any registered user tools. Opt out for one
call withInvoke-Shp -DisableMcp; nothing is offered until you attach a
server, so the default posture is unchanged.
Both protocol eras are supported over stdio. Verified from the
specification rather than from memory, which changed the design: the current
revision 2026-07-28 removed theinitializehandshake entirely - a modern
request is stateless and carries its protocol version and client capabilities
in_meta, withserver/discoveras a mandatory RPC - while nearly every
server in the field still expects the handshake. The client therefore probes
withserver/discoverand falls back toinitializeon any other error or a
timeout, never keyed to one error code. Both eras were negotiated live, and
the real Azure MCP Server 2.0.5 turned out to be a legacy server - which
settles whether supporting both was worth it.
See specs/021-mcp-server-support.md.- Tool names are namespaced
mcp_<alias>_<tool>using the alias you chose,
not the server's self-reported name, which the protocol says nothing
verifies. The sanitiser is written to a measured constraint, not a guessed
one: the Copilot endpoint enforces^[a-zA-Z0-9_-]{1,128}$, so a dot - which
MCP explicitly permits in a tool name - becomes_. This matters because a
rejected name identifies the offending tool only by its index in the
request, and the resulting 400 is masked by the chat-to-responses fallback,
surfacing as "model ... does not support Responses API" - a true statement
about an entirely different problem. Get-ShpToolnow lists MCP tools next to user tools, with a newOrigin
column (UserorMcp) and the contributingServer.Invoke-Shpreports
McpEnabled,McpServersAvailable,McpToolsAvailableandMcpToolsCalled,
and an MCP call emits the sameToolCallprogress record as every other tool,
so a host renders it identically.
Security
- An MCP server is a third-party process running with your privileges, and
there is no sandbox. Its tool names and descriptions are untrusted input the
model reads on every round-trip, and its results are untrusted content. Three
controls follow, each stated with its limits:- Nothing is discovered. No scan of the working directory,
.vscodeor a
user profile.Register-ShpMcpServer -Pathreads a file you name. A
configuration file is a command line, and a command line is arbitrary code. - The tool list is frozen at registration. The client opens no
subscriptions/listenstream, so it receives no
notifications/tools/list_changedand a server cannot add or alter tools
after you approved them. Refreshing is an explicit-Force. This does not
stop a server changing its behaviour. - The child environment is built, not inherited. Unlike
run_command,
which deliberately inherits the whole block for compatibility, an MCP child
starts from a minimal base plus exactly the variables you name in
-Environment, so an ambient$env:credential is not handed to somebody
else's process.
- Nothing is discovered. No scan of the working directory,
Set-ShpToolPolicycannot gate an MCP call, and this is stated rather than
implied. Its rules match resolved filesystem paths and leading command
tokens; a tool call has neither. Demonstrated in one live Turn under
Read(<repo>/**): the built-inread_filewas denied with a reason and the
MCP tool call ran. A policy that scopesread_fileto one directory does
nothing about an attached filesystem server. Reduce reach at attachment
instead, with-ToolName.- The injection path was measured, not asserted. With a hostile instruction in a
tool description only, the model read a decoy credentials file and passed
its contents to the third-party server as a tool argument - the server's own
log confirms receipt. The same run with-DisableFileAccessread nothing and
leaked nothing. - A configuration entry asking for
sandboxEnabledwarns and still starts:
a configuration written for a sandboxing host is exactly the one you want to
reuse. The gap is surfaced twice rather than being fatal once - a warning that
says the server is starting unsandboxed, plusSandboxRequestedon the server
record, because a warning scrolls away and a property does not. An entry
carrying an unresolved${...}variable is refused, because starting the
literal text would run a different command than the file describes. Get-ShpMcpServerreports environment variable names only, never values.
Known limits
-
stdio transport only. Streamable HTTP is deferred with the MCP Authorization
framework; a half-authorised HTTP client would be worse than none. -
Invoke-ShpBatchdoes not use attached servers and warns once. A worker
runspace inherits nothing, so replaying an attachment would start one copy of
every server per worker. -
A server that exits is marked
Faultedrather than restarted automatically.
The specification says a client SHOULD restart one; automatic respawn of
third-party code inside an unattended loop turns one crash into a crash loop
nobody is watching. Re-attach withRegister-ShpMcpServer -Force. -
Resources, prompts, sampling, elicitation and roots are out of scope for this
version. Client capabilities are declared empty, so a modern server needing
one gets a protocol error naming it rather than hanging. -
The cached OAuth token is now protected at rest, closing open decision #5.
On Windows it is DPAPI-encrypted for your account; on every platform the file
is restricted to the current user. Measured before the change, the real file
was 40 bytes of clear text with the profile's inherited ACL, readable by
BUILTIN\Administratorsas well as by you.
The file is self-describing -SHPv1:DPAPI:...orSHPv1:NONE:...- and
Initialize-Shpreports which protection it applied, because a scheme that
silently degrades to clear text is worse than clear text. On Linux and macOS
there is no DPAPI equivalent without a dependency, so the scheme isNONE
and file permissions (mode 600) are the only control; that is stated in the
file and on screen rather than implied.
No runtime dependency was added -ConvertTo-SecureStringis built in.
SecretManagement was considered and rejected: SecretStore prompts to unlock,
which would break every unattended run, and configuring it not to prompt
reduces it to file permissions while costing this module its empty dependency
list. This buys protection against another principal on the machine; it does
not protect against code running as you, and no candidate scheme would.
See specs/020-encrypted-token-storage.md. -
An existing clear-text token file keeps working and is upgraded in place by
Initialize-Shpwithout re-authenticating, so nobody needs a browser just
to gain protection. A protected file that cannot be decrypted - typically one
copied from another machine or account - throws with an actionable message
namingInitialize-Shp -Force, rather than sending a ciphertext to the
service as a bearer token. -
Set-ShpToolPolicy,Get-ShpToolPolicyandClear-ShpToolPolicyscope what
the unsandboxed file and shell tools may reach, so an unattended run can be
given the access it needs instead of the caller's entire filesystem and shell.
Rules are writtenRead(./src/**),Write(./out/**),Shell(git status),
with a leading!to deny; any matching deny beats every matching allow.
Nothing changes until you set a policy - and once you do, the model is
denied by default. Refusals are reported on the result asToolCallsDenied,
and the policy travels into everyInvoke-ShpBatchworker, because a worker
inherits no module state and would otherwise be the one unguarded path.
This closes a real gap rather than a symmetry:-Confirmis interactive only
so an unattended run never prompts, and-DisableFileAccess/
-DisableTerminalare all-or-nothing, so the only safe unattended setting was
"all tools off".
See specs/019-tool-access-policy.md. -
Paths are matched on the absolute, link-resolved path rather than the
string the model supplied, so neither a..segment nor a directory junction
walks out of an allowed root, and patterns are anchored so a rule forout
cannot matchoutsider.run_commandis matched on whole leading tokens and
refuses any shell metacharacter whatever the rules say, because
git status; curl ...would otherwise pass a rule that only ever meant
git status. AShellrule constrains which program runs, not what it does;
the spec states that limit and the others plainly. -
Invoke-Shp -RetryDelaySec, and-TimeoutSec,-MaxRetryCount,
-RetryDelaySecand-NetworkOutageToleranceSeconGet-ShpModeland
Request-ShpEmbedding. All four now resolve identically everywhere - explicit
parameter, thenSet-ShpContext, then the built-in default - and apply to
every request the module makes, including the OAuth-to-session token
exchange,/modelsand embeddings. Those three previously called the retry
wrapper with the module's own defaults, soSet-ShpContext -TimeoutSec 10 -MaxRetryCount 0silently did not apply to them; andInvoke-Shpresolved
its options after the token exchange had already run, so an explicit
-TimeoutSecnever reached the one request that gates every other one. There
is deliberately no exemption for the auth handshake: the exchange is cached,
so-MaxRetryCount 0costs at most one un-retried attempt per ...