ShellPilot.psd1

#
# Module manifest for module 'ShellPilot'
#
# Generated by: raandree
#
# Generated on: 6/6/2026
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'ShellPilot.psm1'

# Version number of this module.
ModuleVersion = '0.4.0'

# Supported PSEditions
# CompatiblePSEditions = @()

# ID used to uniquely identify this module
GUID = 'd2a14b3e-8f6e-4a07-9c2d-1e5a6e3b9c01'

# Author of this module
Author = 'raandree'

# Company or vendor of this module
CompanyName = 'raandree'

# Copyright statement for this module
Copyright = '(c) raandree. All rights reserved.'

# Description of the functionality provided by this module
Description = 'GitHub Copilot in your PowerShell terminal: device-flow auth, model listing, chat and agentic tool-calling with usage and cost.'

# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '7.0'

# Name of the PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# ClrVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @()

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = @('ShellPilot.Format.ps1xml')

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Clear-ShpChat','Clear-ShpContext','Clear-ShpToolPolicy','Clear-ShpUsage','Compress-ShpChat','ConvertTo-ShpTokenCount','Get-ShpChat','Get-ShpContext','Get-ShpCosineSimilarity','Get-ShpCostEstimate','Get-ShpDefault','Get-ShpMcpServer','Get-ShpModel','Get-ShpModelName','Get-ShpTool','Get-ShpToolPolicy','Get-ShpUsage','Initialize-Shp','Invoke-Shp','Invoke-ShpBatch','Register-ShpMcpServer','Register-ShpTool','Request-ShpEmbedding','Resolve-ShpError','Select-ShpModel','Set-ShpContext','Set-ShpToolPolicy','Start-ShpChat','Unregister-ShpMcpServer','Unregister-ShpTool')

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()

# Variables to export from this module
VariablesToExport = @()

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()

# DSC resources to export from this module
DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        Tags = @('GitHubCopilot', 'Copilot', 'AI', 'LLM', 'Chat', 'Agent', 'PSEdition_Core')

        # A URL to the license for this module.
        LicenseUri = 'https://github.com/raandree/ShellPilot/blob/main/LICENSE'

        # A URL to the main website for this project.
        ProjectUri = 'https://github.com/raandree/ShellPilot'

        # A URL to an icon representing this module.
        IconUri = 'https://raw.githubusercontent.com/raandree/ShellPilot/main/assets/shellpilot-icon.png'

        # ReleaseNotes of this module
        ReleaseNotes = '## [0.4.0-preview0007] - 2026-08-12

### 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](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](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](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 att'


        # Prerelease string of this module
        Prerelease =  'preview0007'

        # Flag to indicate whether the module requires explicit user acceptance for install/update/save
        # RequireLicenseAcceptance = $false

        # External dependent modules of this module
        # ExternalModuleDependencies = @()

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}