Skip to content

Releases: raandree/ShellPilot

v0.4.0-preview0016

v0.4.0-preview0016 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 30 Sep 07:05

[v0.4.0-preview0016]

Security

  • Gate every tool class from one policy. Set-ShpToolPolicy gains Url, Mcp
    and Tool rule kinds beside Read, Write and Shell, each matched
    against a resolved form rather than the string the model supplied: a
    normalised address, the alias/tool a call will dispatch under, and an exact
    tool name. A deny beats every matching allow. Name the
    RestrictedUnattended trust profile to enforce all six kinds at once, so an
    unattended run may do only what is listed. An Mcp rule 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, in Invoke-Shp and Request-ShpEmbedding alike. A
    pipeline pointed at its own endpoint therefore needs no GitHub sign-in at
    all, and Test-ShpCiReadiness reports TokenSource as NotRequired
    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-ShpSubagent can 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_command to a minimal environment and refuse execution-sensitive
    literal environment assignments before child startup, even without a Tool
    policy. See command environment.

  • Protect Unix edit_file staging 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_file on 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, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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-ShpEval runs 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-ShpSubagent for 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 -ToolCallControl and -ExecutionContract to Invoke-Shp and
    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's ToolCallDecisions as a receipt and as a tool.decision
    Event, so an unattended run can show afterwards what was approved, what was
    rewritten and what was refused.

  • Add Get-ShpContextReport to price a call before sending it, and
    Invoke-Shp -ContextReport / Invoke-ShpBatch -ContextReport to 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 -Focus to 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 -Focus compacts exactly as before.

  • Add -ToolResultSpillRoot to Invoke-Shp and Invoke-ShpBatch so 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 with read_file's existing offset and limit.

  • Add Save-ShpChat, Restore-ShpChat and Get-ShpChatCheckpoint to save a
    conversation, inspect its checkpoints, resume it, and roll it back to an
    earlier turn, plus Invoke-Shp -SaveChatPath to 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...

Read more

v0.4.0-preview0015

v0.4.0-preview0015 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 25 Sep 00:32

[v0.4.0-preview0015]

Security

  • Gate every tool class from one policy. Set-ShpToolPolicy gains Url, Mcp
    and Tool rule kinds beside Read, Write and Shell, each matched
    against a resolved form rather than the string the model supplied: a
    normalised address, the alias/tool a call will dispatch under, and an exact
    tool name. A deny beats every matching allow. Name the
    RestrictedUnattended trust profile to enforce all six kinds at once, so an
    unattended run may do only what is listed. An Mcp rule 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, in Invoke-Shp and Request-ShpEmbedding alike. A
    pipeline pointed at its own endpoint therefore needs no GitHub sign-in at
    all, and Test-ShpCiReadiness reports TokenSource as NotRequired
    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-ShpSubagent can 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_command to a minimal environment and refuse execution-sensitive
    literal environment assignments before child startup, even without a Tool
    policy. See command environment.

  • Protect Unix edit_file staging 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_file on 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, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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-ShpEval runs 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-ShpSubagent for 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 -ToolCallControl and -ExecutionContract to Invoke-Shp and
    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's ToolCallDecisions as a receipt and as a tool.decision
    Event, so an unattended run can show afterwards what was approved, what was
    rewritten and what was refused.

  • Add Get-ShpContextReport to price a call before sending it, and
    Invoke-Shp -ContextReport / Invoke-ShpBatch -ContextReport to 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 -Focus to 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 -Focus compacts exactly as before.

  • Add -ToolResultSpillRoot to Invoke-Shp and Invoke-ShpBatch so 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 with read_file's existing offset and limit.

  • Add Save-ShpChat, Restore-ShpChat and Get-ShpChatCheckpoint to save a
    conversation, inspect its checkpoints, resume it, and roll it back to an
    earlier turn, plus Invoke-Shp -SaveChatPath to 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...

Read more

v0.4.0-preview0014

v0.4.0-preview0014 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 07 Sep 18:26

[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_command to a minimal environment and refuse execution-sensitive
    literal environment assignments before child startup, even without a Tool
    policy. See command environment.

  • Protect Unix edit_file staging 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_file on 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, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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 -DeferredToolLoading to Invoke-Shp and Invoke-ShpBatch for
    Turn-local User/MCP schema search with search_tools. Keep fixed built-ins
    and explicit -Tool selections eager, with exclusion winning. Reserve the
    search_tools registration 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, and SHELLPILOT_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 Plan read-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 -Tool and -ExcludeTool visibility filters to Invoke-Shp and
    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 -SecretEnvironmentVariable for 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-estimate for 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-Shp transport.
    RequestLimits and a trusted RequestTokenCounter reserve 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
    NoAutomaticRetry to disable transport and API-shape resends, or
    RequestTransport for 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_file for targeted changes to existing files. Replace one
    exact, case-sensitive oldString with newString, 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 a Write() rule
    covering the target, and a deny in either kind refuses it. Match counts
    disclose file content even when the replacement is identical, so Write()
    alone would turn the tool into a way to confirm guesses about a file the
    policy never granted read access to. write_file and create_directory are
    unchanged and still need only Write().

    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 reports recoveryPath for 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_files and grep_files let the model search without a shell.
    glob_files finds files by name pattern under a directory; grep_files
    searches their contents and returns only the path, line number and matching
    line, leaving read_file to read around a hit. Both are offered with the
    other file tools and withdrawn by -DisableFileAccess.

    Both are governed by the existing Read() rules of Set-ShpToolPolicy, and
    that is the point: until now the only way to make the model find something
    was run_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 while run_command stays 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
    excludedByPolicy rather than dropped silently. Both results are bounded —
    files examined, matches returned, and characters returned — and set
    truncated when any cap bites, so the model narrows the pattern instead of
    overflowing the context window.

  • ConvertTo-ShpAnnotation surfaces structured findings in CI. Pipe a
    ShellPilot.Result from Invoke-Shp -JsonSchema, or any plain finding
    object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
    task.logissue commands, or readable text. Level, Path, Line,
    Column, Title, and Message are 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
    -Emit writes it to the host, and -Summary appe...

Read more

v0.4.0-preview0013

v0.4.0-preview0013 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 07 Sep 09:27

[v0.4.0-preview0013]

Security

  • Protect Unix edit_file staging 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_file on 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, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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-estimate for 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-Shp transport.
    RequestLimits and a trusted RequestTokenCounter reserve 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
    NoAutomaticRetry to disable transport and API-shape resends, or
    RequestTransport for 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_file for targeted changes to existing files. Replace one
    exact, case-sensitive oldString with newString, 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 a Write() rule
    covering the target, and a deny in either kind refuses it. Match counts
    disclose file content even when the replacement is identical, so Write()
    alone would turn the tool into a way to confirm guesses about a file the
    policy never granted read access to. write_file and create_directory are
    unchanged and still need only Write().

    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 reports recoveryPath for 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_files and grep_files let the model search without a shell.
    glob_files finds files by name pattern under a directory; grep_files
    searches their contents and returns only the path, line number and matching
    line, leaving read_file to read around a hit. Both are offered with the
    other file tools and withdrawn by -DisableFileAccess.

    Both are governed by the existing Read() rules of Set-ShpToolPolicy, and
    that is the point: until now the only way to make the model find something
    was run_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 while run_command stays 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
    excludedByPolicy rather than dropped silently. Both results are bounded —
    files examined, matches returned, and characters returned — and set
    truncated when any cap bites, so the model narrows the pattern instead of
    overflowing the context window.

  • ConvertTo-ShpAnnotation surfaces structured findings in CI. Pipe a
    ShellPilot.Result from Invoke-Shp -JsonSchema, or any plain finding
    object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
    task.logissue commands, or readable text. Level, Path, Line,
    Column, Title, and Message are 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
    -Emit writes it to the host, and -Summary appends a Markdown table to
    $env:GITHUB_STEP_SUMMARY when 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 -MaxBudgetUSD left one object saying
    BudgetExceeded = $true and 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 monotonic sequence, an ISO 8601 UTC timestamp, a
    type and a flat data object. 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; a run_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 a retry or error.
    Transient HTTP and network-outage retries from the shared request wrapper are
    recorded with attempt, delay and status data, and an invented ask_user call
    in a non-interactive turn records its denied Tool call and terminal error
    before the call stops. -DisableProgressEvents no longer switches this off -
    the two sinks are gated independently.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp -AsJob and Invoke-ShpBatch -AsJob run a call in the
    background.
    Both return a thread job whose Receive-Job resolves to the
    same ShellPilot.Result / ShellPilot.BatchResult objects the synchronous
    call returns - the same process, so nothing is serialised into a
    `Deserialized...

Read more

v0.4.0-preview0012

v0.4.0-preview0012 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 06 Sep 20:31

[v0.4.0-preview0012]

Security

  • Protect Unix edit_file staging 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_file on 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, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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_file for targeted changes to existing files. Replace one
    exact, case-sensitive oldString with newString, 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 a Write() rule
    covering the target, and a deny in either kind refuses it. Match counts
    disclose file content even when the replacement is identical, so Write()
    alone would turn the tool into a way to confirm guesses about a file the
    policy never granted read access to. write_file and create_directory are
    unchanged and still need only Write().

    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 reports recoveryPath for 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_files and grep_files let the model search without a shell.
    glob_files finds files by name pattern under a directory; grep_files
    searches their contents and returns only the path, line number and matching
    line, leaving read_file to read around a hit. Both are offered with the
    other file tools and withdrawn by -DisableFileAccess.

    Both are governed by the existing Read() rules of Set-ShpToolPolicy, and
    that is the point: until now the only way to make the model find something
    was run_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 while run_command stays 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
    excludedByPolicy rather than dropped silently. Both results are bounded —
    files examined, matches returned, and characters returned — and set
    truncated when any cap bites, so the model narrows the pattern instead of
    overflowing the context window.

  • ConvertTo-ShpAnnotation surfaces structured findings in CI. Pipe a
    ShellPilot.Result from Invoke-Shp -JsonSchema, or any plain finding
    object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
    task.logissue commands, or readable text. Level, Path, Line,
    Column, Title, and Message are 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
    -Emit writes it to the host, and -Summary appends a Markdown table to
    $env:GITHUB_STEP_SUMMARY when 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 -MaxBudgetUSD left one object saying
    BudgetExceeded = $true and 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 monotonic sequence, an ISO 8601 UTC timestamp, a
    type and a flat data object. 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; a run_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 a retry or error.
    Transient HTTP and network-outage retries from the shared request wrapper are
    recorded with attempt, delay and status data, and an invented ask_user call
    in a non-interactive turn records its denied Tool call and terminal error
    before the call stops. -DisableProgressEvents no longer switches this off -
    the two sinks are gated independently.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp -AsJob and Invoke-ShpBatch -AsJob run a call in the
    background.
    Both return a thread job whose Receive-Job resolves to the
    same ShellPilot.Result / ShellPilot.BatchResult objects 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 -AsJob is 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: -AsJob does not silently turn it off.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp now 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 ...

Read more

v0.4.0-preview0011

v0.4.0-preview0011 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 06 Sep 08:35

[v0.4.0-preview0011]

Security

  • A disabled tool can no longer be executed. -DisableTerminal,
    -DisableFileAccess, -DisableBrowsing, -DisableUserPrompts and
    -DisableTodoList removed 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. -DisableTerminal bounded 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: the tool.call
    event carries policy = denied, the reason names the disabled tool, the call
    appears on ToolCallsDenied, 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-ShpTool refuses a built-in tool name. Dispatch matches built-in
    names before it consults the user tool table, so registering run_command,
    read_file or 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_files and grep_files let the model search without a shell.
    glob_files finds files by name pattern under a directory; grep_files
    searches their contents and returns only the path, line number and matching
    line, leaving read_file to read around a hit. Both are offered with the
    other file tools and withdrawn by -DisableFileAccess.

    Both are governed by the existing Read() rules of Set-ShpToolPolicy, and
    that is the point: until now the only way to make the model find something
    was run_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 while run_command stays 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
    excludedByPolicy rather than dropped silently. Both results are bounded —
    files examined, matches returned, and characters returned — and set
    truncated when any cap bites, so the model narrows the pattern instead of
    overflowing the context window.

  • ConvertTo-ShpAnnotation surfaces structured findings in CI. Pipe a
    ShellPilot.Result from Invoke-Shp -JsonSchema, or any plain finding
    object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
    task.logissue commands, or readable text. Level, Path, Line,
    Column, Title, and Message are 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
    -Emit writes it to the host, and -Summary appends a Markdown table to
    $env:GITHUB_STEP_SUMMARY when 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 -MaxBudgetUSD left one object saying
    BudgetExceeded = $true and 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 monotonic sequence, an ISO 8601 UTC timestamp, a
    type and a flat data object. 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; a run_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 a retry or error.
    Transient HTTP and network-outage retries from the shared request wrapper are
    recorded with attempt, delay and status data, and an invented ask_user call
    in a non-interactive turn records its denied Tool call and terminal error
    before the call stops. -DisableProgressEvents no longer switches this off -
    the two sinks are gated independently.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp -AsJob and Invoke-ShpBatch -AsJob run a call in the
    background.
    Both return a thread job whose Receive-Job resolves to the
    same ShellPilot.Result / ShellPilot.BatchResult objects 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 -AsJob is 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: -AsJob does not silently turn it off.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp now 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 same Name(Pattern)
    shape Set-ShpToolPolicy already uses, and the custom policy travels to
    every Invoke-ShpBatch worker the same way the tool policy does. Redaction
    is on by default; pass -DisableRedaction to 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
    -JsonSchema reply still parses onto ContentObject exactly 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-ShpBatch and Initialize-Shp take
    -NonInteractive, on automatically when $env:CI is truthy and overridable
    with -NonInteractive:$false. It withdraws ask_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 calls ask_user anyway ends the turn with
    ShpNonInteractivePrompt rather 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
    ShpCopilotBackendInCi and 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_BASE and $env:SHELLPILOT_API_KEY are read as
    backend defaults, below an explicit -ApiBase and 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-ShpCiReadiness reports the whole res...

Read more

v0.4.0-preview0010

v0.4.0-preview0010 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 26 Aug 19:22

[v0.4.0-preview0010]

Added

  • ConvertTo-ShpAnnotation surfaces structured findings in CI. Pipe a
    ShellPilot.Result from Invoke-Shp -JsonSchema, or any plain finding
    object, into the cmdlet to produce GitHub Actions annotations, Azure DevOps
    task.logissue commands, or readable text. Level, Path, Line,
    Column, Title, and Message are 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
    -Emit writes it to the host, and -Summary appends a Markdown table to
    $env:GITHUB_STEP_SUMMARY when 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 -MaxBudgetUSD left one object saying
    BudgetExceeded = $true and 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 monotonic sequence, an ISO 8601 UTC timestamp, a
    type and a flat data object. 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; a run_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 a retry or error.
    Transient HTTP and network-outage retries from the shared request wrapper are
    recorded with attempt, delay and status data, and an invented ask_user call
    in a non-interactive turn records its denied Tool call and terminal error
    before the call stops. -DisableProgressEvents no longer switches this off -
    the two sinks are gated independently.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp -AsJob and Invoke-ShpBatch -AsJob run a call in the
    background.
    Both return a thread job whose Receive-Job resolves to the
    same ShellPilot.Result / ShellPilot.BatchResult objects 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 -AsJob is 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: -AsJob does not silently turn it off.
    See specs/027-headless-event-stream.md.

  • Invoke-Shp now 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 same Name(Pattern)
    shape Set-ShpToolPolicy already uses, and the custom policy travels to
    every Invoke-ShpBatch worker the same way the tool policy does. Redaction
    is on by default; pass -DisableRedaction to 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
    -JsonSchema reply still parses onto ContentObject exactly 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-ShpBatch and Initialize-Shp take
    -NonInteractive, on automatically when $env:CI is truthy and overridable
    with -NonInteractive:$false. It withdraws ask_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 calls ask_user anyway ends the turn with
    ShpNonInteractivePrompt rather 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
    ShpCopilotBackendInCi and 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_BASE and $env:SHELLPILOT_API_KEY are read as
    backend defaults, below an explicit -ApiBase and 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-ShpCiReadiness reports 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-Shp with 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 no ApiKey was
    configured. That was reachable before only through Set-ShpContext; reading
    ApiBase from 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
    Authorization header at all, which is what a local server expects anyway.
    A URL's credentials are also redacted from the result's Endpoint member.

  • A pipeline step can now fail when the call did not deliver. A budget
    overrun was a Write-Warning plus a BudgetExceeded property, so an
    unattended run exited 0 on a truncated or abandoned answer and wrote the
    half-finished artifact anyway. Invoke-Shp -FailOn turns five named outcomes
    into terminating errors: BudgetExceeded, Truncated, ToolIterationLimit,
    NoContent and SchemaMismatch.
    Each carries a distinct, documented FullyQualifiedErrorId
    (ShpBudgetExceeded,Invoke-Shp and so on) so a wrapper branches on the
    condition instead of matching an English message - which is what
    MaxToolIterations forced, having used its own message text as the error id.
    Omitting -FailOn changes 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 -FailOn decides only
    whether the call ends with a result or with an error. The whole
    ShellPilot.Result rides on ErrorRecord.TargetObject, so a catch block
    still knows what the abandoned turn cost.
    ShellPilot never sets $LASTEXITCODE and never calls exit - a module that
    terminates its host cannot be composed - so the exit code stays the caller's
    job; the comment-based help carries the try/catch plus exit 1 wrapper.
    See specs/024-pipeline-failure-semantics.md.

  • Invoke-ShpBatch -FailOn and -FailBatchOnAnyItem. The same five
    conditions apply per item, and a tripped one never aborts the batch: the item
    reports Success = $false with the branchable ErrorRecord intact while every
    other item runs to completion. `...

Read more

v0.4.0-preview0009

v0.4.0-preview0009 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 24 Aug 19:35

[v0.4.0-preview0009]

Added

  • Invoke-Shp -Attachment takes a file of any format. -Image was 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, because read_file decodes text and a .msg comes
    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 has read_file
    and run_command and 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 e1 the 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 reports Attachments.
    See specs/022-file-attachments.md.

  • Register-ShpMcpServer, Get-ShpMcpServer and Unregister-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 with Invoke-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 the initialize handshake entirely - a modern
    request is stateless and carries its protocol version and client capabilities
    in _meta, with server/discover as a mandatory RPC - while nearly every
    server in the field still expects the handshake. The client therefore probes
    with server/discover and falls back to initialize on 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-ShpTool now lists MCP tools next to user tools, with a new Origin
    column (User or Mcp) and the contributing Server. Invoke-Shp reports
    McpEnabled, McpServersAvailable, McpToolsAvailable and McpToolsCalled,
    and an MCP call emits the same ToolCall progress 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, .vscode or a
      user profile. Register-ShpMcpServer -Path reads 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/listen stream, so it receives no
      notifications/tools/list_changed and 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.
  • Set-ShpToolPolicy cannot 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-in read_file was denied with a reason and the
    MCP tool call ran. A policy that scopes read_file to 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 -DisableFileAccess read nothing and
    leaked nothing.
  • A configuration entry asking for sandboxEnabled warns 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, plus SandboxRequested on 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-ShpMcpServer reports 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-ShpBatch does 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 Faulted rather 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 with Register-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\Administrators as well as by you.
    The file is self-describing - SHPv1:DPAPI:... or SHPv1:NONE:... - and
    Initialize-Shp reports 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 is NONE
    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-SecureString is 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-Shp without 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
    naming Initialize-Shp -Force, rather than sending a ciphertext to the
    service as a bearer token.

  • Set-ShpToolPolicy, Get-ShpToolPolicy and Clear-ShpToolPolicy scope 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 written Read(./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 as ToolCallsDenied,
    and the policy travels into every Invoke-ShpBatch worker, because a worker
    inherits no module state and would otherwise be the one unguarded path.
    This closes a real gap rather than a symmetry: -Confirm is interactive...

Read more

v0.4.0-preview0008

v0.4.0-preview0008 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 19 Aug 10:35

[v0.4.0-preview0008]

Added

  • Register-ShpMcpServer, Get-ShpMcpServer and Unregister-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 with Invoke-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 the initialize handshake entirely - a modern
    request is stateless and carries its protocol version and client capabilities
    in _meta, with server/discover as a mandatory RPC - while nearly every
    server in the field still expects the handshake. The client therefore probes
    with server/discover and falls back to initialize on 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-ShpTool now lists MCP tools next to user tools, with a new Origin
    column (User or Mcp) and the contributing Server. Invoke-Shp reports
    McpEnabled, McpServersAvailable, McpToolsAvailable and McpToolsCalled,
    and an MCP call emits the same ToolCall progress 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, .vscode or a
      user profile. Register-ShpMcpServer -Path reads 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/listen stream, so it receives no
      notifications/tools/list_changed and 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.
  • Set-ShpToolPolicy cannot 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-in read_file was denied with a reason and the
    MCP tool call ran. A policy that scopes read_file to 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 -DisableFileAccess read nothing and
    leaked nothing.
  • A configuration entry asking for sandboxEnabled warns 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, plus SandboxRequested on 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-ShpMcpServer reports 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-ShpBatch does 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 Faulted rather 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 with Register-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\Administrators as well as by you.
    The file is self-describing - SHPv1:DPAPI:... or SHPv1:NONE:... - and
    Initialize-Shp reports 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 is NONE
    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-SecureString is 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-Shp without 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
    naming Initialize-Shp -Force, rather than sending a ciphertext to the
    service as a bearer token.

  • Set-ShpToolPolicy, Get-ShpToolPolicy and Clear-ShpToolPolicy scope 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 written Read(./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 as ToolCallsDenied,
    and the policy travels into every Invoke-ShpBatch worker, because a worker
    inherits no module state and would otherwise be the one unguarded path.
    This closes a real gap rather than a symmetry: -Confirm is interactive only
    so an unattended run never prompts, and -DisableFileAccess /
    -DisableTerminal are 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 for out
    cannot match outsider. run_command is 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. A Shell rule constrains which program runs, not what it does;
    the spec states that limit and the others plainly.

  • Invoke-Shp -RetryDelaySec, and -TimeoutSec, -MaxRetryCount,
    -RetryDelaySec and -NetworkOutageToleranceSec on Get-ShpModel and
    Request-ShpEmbedding. All four now resolve identically everywhere - explicit
    parameter, then Set-ShpContext, then the built-in default - and apply to
    every request the module makes, including the OAuth-to-session token
    exchange, /models and embeddings. Those three previously called the retry
    wrapper with the module's own defaults, so Set-ShpContext -TimeoutSec 10 -MaxRetryCount 0 silently did not apply to them; and Invoke-Shp resolved
    its options after the token exchange had already run, so an explicit
    -TimeoutSec never reached the one request that gates every other one. There
    is deliberately no exemption for the auth handshake: the exchange is cached,
    so -MaxRetryCount 0 costs at most one un-retried attempt per ...

Read more

v0.4.0-preview0007

v0.4.0-preview0007 Pre-release
Pre-release

Choose a tag to compare

@raandree raandree released this 12 Aug 19:04

[v0.4.0-preview0007]

Added

  • Register-ShpMcpServer, Get-ShpMcpServer and Unregister-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 with Invoke-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 the initialize handshake entirely - a modern
    request is stateless and carries its protocol version and client capabilities
    in _meta, with server/discover as a mandatory RPC - while nearly every
    server in the field still expects the handshake. The client therefore probes
    with server/discover and falls back to initialize on 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-ShpTool now lists MCP tools next to user tools, with a new Origin
    column (User or Mcp) and the contributing Server. Invoke-Shp reports
    McpEnabled, McpServersAvailable, McpToolsAvailable and McpToolsCalled,
    and an MCP call emits the same ToolCall progress 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, .vscode or a
      user profile. Register-ShpMcpServer -Path reads 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/listen stream, so it receives no
      notifications/tools/list_changed and 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.
  • Set-ShpToolPolicy cannot 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-in read_file was denied with a reason and the
    MCP tool call ran. A policy that scopes read_file to 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 -DisableFileAccess read nothing and
    leaked nothing.
  • A configuration entry asking for sandboxEnabled warns 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, plus SandboxRequested on 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-ShpMcpServer reports 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-ShpBatch does 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 Faulted rather 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 with Register-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\Administrators as well as by you.
    The file is self-describing - SHPv1:DPAPI:... or SHPv1:NONE:... - and
    Initialize-Shp reports 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 is NONE
    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-SecureString is 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-Shp without 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
    naming Initialize-Shp -Force, rather than sending a ciphertext to the
    service as a bearer token.

  • Set-ShpToolPolicy, Get-ShpToolPolicy and Clear-ShpToolPolicy scope 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 written Read(./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 as ToolCallsDenied,
    and the policy travels into every Invoke-ShpBatch worker, because a worker
    inherits no module state and would otherwise be the one unguarded path.
    This closes a real gap rather than a symmetry: -Confirm is interactive only
    so an unattended run never prompts, and -DisableFileAccess /
    -DisableTerminal are 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 for out
    cannot match outsider. run_command is 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. A Shell rule constrains which program runs, not what it does;
    the spec states that limit and the others plainly.

  • Invoke-Shp -RetryDelaySec, and -TimeoutSec, -MaxRetryCount,
    -RetryDelaySec and -NetworkOutageToleranceSec on Get-ShpModel and
    Request-ShpEmbedding. All four now resolve identically everywhere - explicit
    parameter, then Set-ShpContext, then the built-in default - and apply to
    every request the module makes, including the OAuth-to-session token
    exchange, /models and embeddings. Those three previously called the retry
    wrapper with the module's own defaults, so Set-ShpContext -TimeoutSec 10 -MaxRetryCount 0 silently did not apply to them; and Invoke-Shp resolved
    its options after the token exchange had already run, so an explicit
    -TimeoutSec never reached the one request that gates every other one. There
    is deliberately no exemption for the auth handshake: the exchange is cached,
    so -MaxRetryCount 0 costs at most one un-retried attempt per ...

Read more