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.4'

# 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-ShpRedactionPolicy','Clear-ShpToolPolicy','Clear-ShpUsage','Compress-ShpChat','ConvertTo-ShpAnnotation','ConvertTo-ShpTokenCount','Get-ShpChat','Get-ShpContext','Get-ShpCosineSimilarity','Get-ShpCostEstimate','Get-ShpDefault','Get-ShpMcpServer','Get-ShpModel','Get-ShpModelName','Get-ShpRedactionPolicy','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-ShpRedactionPolicy','Set-ShpToolPolicy','Start-ShpChat','Test-ShpCiReadiness','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-preview0012] - 2026-09-06

### 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](README.md#agent-tools-on-by-default).

- **`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](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](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](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
 '


        # Prerelease string of this module
        Prerelease =  'preview0012'

        # 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 = ''

}