copilot-operator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/dist/src/api/app.module.js +18 -0
  4. package/dist/src/api/dataAcl.js +109 -0
  5. package/dist/src/api/folderPicker.js +110 -0
  6. package/dist/src/api/main.js +132 -0
  7. package/dist/src/api/operator.controller.js +975 -0
  8. package/dist/src/api/operator.service.js +1971 -0
  9. package/dist/src/api/saveToDesktop.js +157 -0
  10. package/dist/src/api/security.js +225 -0
  11. package/dist/src/api/settings.js +57 -0
  12. package/dist/src/cli.js +531 -0
  13. package/dist/src/config/layout.js +66 -0
  14. package/dist/src/config/lockedPolicy.js +176 -0
  15. package/dist/src/config/schema.js +487 -0
  16. package/dist/src/context/contextFiles.js +295 -0
  17. package/dist/src/context/desktopMirror.js +113 -0
  18. package/dist/src/context/projectMirror.js +364 -0
  19. package/dist/src/exec/authorizer.js +61 -0
  20. package/dist/src/exec/checks.js +285 -0
  21. package/dist/src/exec/confinement.js +224 -0
  22. package/dist/src/exec/dangerous.js +161 -0
  23. package/dist/src/exec/environment.js +80 -0
  24. package/dist/src/exec/isolation.js +166 -0
  25. package/dist/src/exec/network.js +242 -0
  26. package/dist/src/exec/policy.js +201 -0
  27. package/dist/src/exec/policyManifest.js +84 -0
  28. package/dist/src/exec/processes.js +329 -0
  29. package/dist/src/exec/programs.js +290 -0
  30. package/dist/src/exec/redaction.js +93 -0
  31. package/dist/src/exec/reportFile.js +154 -0
  32. package/dist/src/exec/runner.js +245 -0
  33. package/dist/src/exec/scriptFiles.js +81 -0
  34. package/dist/src/exec/shellExecuteTrap.js +221 -0
  35. package/dist/src/exec/shells.js +298 -0
  36. package/dist/src/exec/stepEnv.js +72 -0
  37. package/dist/src/exec/winps.js +17 -0
  38. package/dist/src/exec/workDir.js +94 -0
  39. package/dist/src/log/runLog.js +43 -0
  40. package/dist/src/orchestrator/derivedChecks.js +95 -0
  41. package/dist/src/orchestrator/review.js +590 -0
  42. package/dist/src/orchestrator/taskRunner.js +1582 -0
  43. package/dist/src/plan/brief.js +1785 -0
  44. package/dist/src/plan/importPlan.js +202 -0
  45. package/dist/src/plan/schema.js +483 -0
  46. package/dist/src/plan/script.js +1012 -0
  47. package/dist/src/plan/systemGuide.js +1851 -0
  48. package/dist/src/protocol/parser.js +224 -0
  49. package/dist/src/protocol/replySchema.js +207 -0
  50. package/dist/src/protocol/reporter.js +75 -0
  51. package/dist/src/protocol/reviewSchema.js +205 -0
  52. package/dist/src/session/compose.js +67 -0
  53. package/dist/src/session/debugExport.js +165 -0
  54. package/dist/src/session/events.js +34 -0
  55. package/dist/src/session/exportRecord.js +261 -0
  56. package/dist/src/session/exports.js +484 -0
  57. package/dist/src/session/model.js +22 -0
  58. package/dist/src/session/retention.js +34 -0
  59. package/dist/src/session/runName.js +36 -0
  60. package/dist/src/session/store.js +440 -0
  61. package/dist/src/session/story.js +198 -0
  62. package/dist/src/transport/acceptance.js +34 -0
  63. package/dist/src/transport/chatSession.js +73 -0
  64. package/dist/src/transport/copilotTransport.js +1208 -0
  65. package/dist/src/transport/edgeCrash.js +59 -0
  66. package/dist/src/transport/locators.js +261 -0
  67. package/dist/src/transport/profileLock.js +102 -0
  68. package/dist/src/util/pacing.js +89 -0
  69. package/dist/src/vcs/commitHygiene.js +102 -0
  70. package/dist/src/vcs/git.js +286 -0
  71. package/dist/src/vcs/taskVcs.js +343 -0
  72. package/dist/web/404.html +15 -0
  73. package/dist/web/_next/static/chunks/12.f1f4d35b52633e8d.js +1 -0
  74. package/dist/web/_next/static/chunks/131-bae98f790a541679.js +1 -0
  75. package/dist/web/_next/static/chunks/197-f7c33a0114fc3db3.js +1 -0
  76. package/dist/web/_next/static/chunks/489.158bbe303a595fba.js +1 -0
  77. package/dist/web/_next/static/chunks/643-34b9f66a6c317c4a.js +1 -0
  78. package/dist/web/_next/static/chunks/867-03b7cac8f79896c3.js +1 -0
  79. package/dist/web/_next/static/chunks/955-63de06e182d98949.js +1 -0
  80. package/dist/web/_next/static/chunks/app/_not-found/page-0d0d8e7e62efca12.js +1 -0
  81. package/dist/web/_next/static/chunks/app/appearance/page-501842e7aeac7f6b.js +1 -0
  82. package/dist/web/_next/static/chunks/app/defaults/page-ed858127cbdfb4ab.js +1 -0
  83. package/dist/web/_next/static/chunks/app/history/page-bad4f3e89b485719.js +1 -0
  84. package/dist/web/_next/static/chunks/app/import/page-db1f5f1ab2f5cad1.js +1 -0
  85. package/dist/web/_next/static/chunks/app/layout-72251c52cc54f95e.js +1 -0
  86. package/dist/web/_next/static/chunks/app/level1/page-cf3e50cf64fecf4f.js +1 -0
  87. package/dist/web/_next/static/chunks/app/page-6fd16e5c8cf3b20f.js +1 -0
  88. package/dist/web/_next/static/chunks/app/presets/page-951e78b80e0e22e6.js +1 -0
  89. package/dist/web/_next/static/chunks/app/sessions/view/page-f2e63618097d7221.js +1 -0
  90. package/dist/web/_next/static/chunks/app/system/page-44e7954c0e48f4ba.js +1 -0
  91. package/dist/web/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js +1 -0
  92. package/dist/web/_next/static/chunks/framework-9ac6a346bd42cedd.js +1 -0
  93. package/dist/web/_next/static/chunks/main-6be4444e2d667e41.js +1 -0
  94. package/dist/web/_next/static/chunks/main-app-e3a5f713ba31557e.js +1 -0
  95. package/dist/web/_next/static/chunks/pages/_app-32d20d5a04949d29.js +1 -0
  96. package/dist/web/_next/static/chunks/pages/_error-abbab53f25ea29b0.js +1 -0
  97. package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  98. package/dist/web/_next/static/chunks/webpack-0f31b926d9a9c5a4.js +1 -0
  99. package/dist/web/_next/static/css/d77735cdc9020a86.css +1 -0
  100. package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_buildManifest.js +1 -0
  101. package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_ssgManifest.js +1 -0
  102. package/dist/web/appearance.html +15 -0
  103. package/dist/web/appearance.txt +26 -0
  104. package/dist/web/defaults.html +15 -0
  105. package/dist/web/defaults.txt +26 -0
  106. package/dist/web/history.html +15 -0
  107. package/dist/web/history.txt +26 -0
  108. package/dist/web/import.html +15 -0
  109. package/dist/web/import.txt +26 -0
  110. package/dist/web/index.html +15 -0
  111. package/dist/web/index.txt +26 -0
  112. package/dist/web/level1.html +15 -0
  113. package/dist/web/level1.txt +26 -0
  114. package/dist/web/presets.html +18 -0
  115. package/dist/web/presets.txt +26 -0
  116. package/dist/web/sessions/view.html +15 -0
  117. package/dist/web/sessions/view.txt +26 -0
  118. package/dist/web/system.html +15 -0
  119. package/dist/web/system.txt +26 -0
  120. package/package.json +111 -0
  121. package/prompts/level1.md +535 -0
  122. package/prompts/organisation.bg.md +44 -0
  123. package/prompts/organisation.en.md +44 -0
  124. package/prompts/persona.bg.md +17 -0
  125. package/prompts/persona.en.md +17 -0
  126. package/prompts/review1.md +277 -0
  127. package/prompts/work.bg.md +19 -0
  128. package/prompts/work.en.md +19 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Markov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,556 @@
1
+ # copilot-operator
2
+
3
+ A Windows bot that runs a task loop with Microsoft 365 Copilot.
4
+
5
+ Copilot decides what to do, the bot does it and reports back, and the two keep going until
6
+ Copilot says the task is finished. The bot drives the real Copilot web app in a real Edge
7
+ browser, so it needs no API licence and no admin consent.
8
+
9
+ > **Status: working, every part verified against a live Microsoft 365 Copilot tenant.**
10
+ > The loop, chat naming, reply parsing, command execution and the results file are confirmed.
11
+ > A step printing for 92 s survived while a silent one was stopped at its idle limit. Project files
12
+ > mirrored to the Desktop were attached and answered questions from. Unattended mode is the
13
+ > one thing still untried, deliberately.
14
+
15
+ ## What it does
16
+
17
+ 1. Opens a conversation and sends **level 1**, the base prompt about how the runner works: phases, json
18
+ format, stop word, the final summary, the rules nothing else can override.
19
+ 2. Sends **level 2**, the user's instructions for the project and the team, together with
20
+ the **task**.
21
+ 3. Reads Copilot's reply and parses a strict JSON block out of it. A reply that does not
22
+ match is sent back with the reason.
23
+ 4. Runs the commands in PowerShell or `cmd`, in order, capturing stdout, stderr and exit codes.
24
+ Commands are the only thing it runs: nothing the chat attaches is fetched, saved or executed.
25
+ 5. Writes the whole terminal output to a `.txt` file and attaches it to the chat.
26
+ 6. Repeats until Copilot finishes with a **summary** of what it did and what the result is.
27
+ 7. Runs the next queued task in the same conversation.
28
+
29
+ A **session** is one conversation with a queue of tasks. Selected parts of a project can be
30
+ mirrored to a Desktop folder and attached, so the chat can see the code it is asked about.
31
+
32
+ There is a web UI for all of this, and a terminal command for a single task.
33
+
34
+ ## Safety
35
+
36
+ The bot executes commands written by a language model — commands only; it has no way to take a
37
+ file from the chat. Confirm mode is on by default: every step is shown and waits for a keypress.
38
+ Unattended mode is behind an explicit flag and its own preconditions. There is a deny list, a
39
+ per-step timeout and an iteration cap. Everything a step prints goes to the chat as a file, so
40
+ secret-shaped strings — tokens, keys, passwords in assignments and URLs, private key blocks —
41
+ are redacted from every report before it is uploaded, always, with `report.redactPatterns`
42
+ applied on top. Running it in a dedicated Windows account or Windows Sandbox is recommended.
43
+
44
+ **Where the reports go.** Every report — the terminal output of every step, the check results,
45
+ the reviewer's transcripts — and any files the project mirror attaches are uploaded into the
46
+ Copilot chat, which stores them in the signed-in account's OneDrive for Business and in the
47
+ Copilot conversation history. They stay inside the company's own Microsoft 365 tenant, under its
48
+ Purview DLP policies, sensitivity labels, retention and eDiscovery; nothing goes to any other
49
+ service. A company deploying this should tell its data-protection and DLP teams so, and set
50
+ `runsRetentionDays` so the local copies under `runs/` follow the same retention rule. The run
51
+ log says it at the start of every run.
52
+
53
+ ### What it will not run
54
+
55
+ Some things are refused whatever the task or the configuration says, because a security team
56
+ watching the machine cannot tell this tool's use of them from an attacker's, and would be right
57
+ not to try. The list lives in `src/exec/dangerous.ts`, is not part of the configuration schema,
58
+ and cannot be edited away; `execution.denyPatterns` is the operator's own list and is applied on
59
+ top of it — it can add, never subtract.
60
+
61
+ | Refused | The ordinary way instead |
62
+ |---|---|
63
+ | `certutil`, `bitsadmin` | read or convert files with PowerShell in a step of their own |
64
+ | `wscript`, `cscript`, `mshta` | run the tool itself; never `.js`, `.vbs` or `.hta` as code |
65
+ | `regsvr32`, `rundll32`, `installutil`, `forfiles` | call the program directly |
66
+ | `-EncodedCommand`, base64 decoded into code, `Invoke-Expression` | write the command out in full |
67
+ | fetching code and running it in one line (`iwr … \| iex`) | write the commands as steps |
68
+ | running anything out of `%TEMP%` | work inside the project folder |
69
+ | antivirus exclusions, scheduled tasks, Run keys, new services | nothing here should outlive the run |
70
+ | opening a file or URL through its registered handler (`Invoke-Item`, `Start-Process` on a document) | start the program by name |
71
+ | writing under `.git\`, or `git config <key> <value>` | the runner owns the repository; read history with git |
72
+ | reaching the runner itself (its ports, its data folder, its key) | nothing: the bot is not part of the task |
73
+
74
+ Every step is a command and every command is read in full before it runs, so there is nowhere for
75
+ any of this to arrive unread. A name hidden rather than written — a caret or empty quotes inside a
76
+ word, a name assembled from pieces, one resolved by wildcard — is refused as the hiding it is. A
77
+ script a step runs with `pwsh -File` is read from disk at the gate and held to the same rules; a
78
+ step that writes a script and runs it in the same line is refused, so that the file read is the
79
+ file run. What `Start-Process` is asked to start is judged like any other program on the line.
80
+
81
+ **Nothing is downloaded on the bot's own account.** A step that fetches from the internet —
82
+ `Invoke-WebRequest`, `Invoke-RestMethod`, `curl`, `wget`, `Start-BitsTransfer`, the .NET web
83
+ clients, `ftp`/`scp`, a package named by URL or git address, `npx` of a tool not installed in the
84
+ project, `docker pull` — is neither refused nor run: it waits on the approval screen, in every
85
+ mode, unattended included, and "run the rest without asking" does not answer it. A check that
86
+ fetches is refused, since a check runs with nobody asked. Requests to this machine (`localhost`,
87
+ `127.0.0.1`) and the project's own package managers are not held. `npm install` runs with
88
+ `ignore-scripts`: a package's install script is code fetched and executed unread, and it does not
89
+ run here.
90
+
91
+ **A step sees a named environment, not the bot's.** Commands are given the variables Windows and
92
+ the toolchains need (`src/exec/stepEnv.ts`) plus whatever `execution.passEnv` names — never the
93
+ API token, never whatever else the operator's shell held. The runner's own git runs with hooks,
94
+ fsmonitor and optional locks off, so nothing a task placed in the repository runs as the runner.
95
+
96
+ ### How a run is regulated
97
+
98
+ Six rules decide what a step may do, and each is recorded with the run that it governed:
99
+
100
+ - **Say where the bot runs, and an unattended run needs an answer.** `execution.isolation` is what
101
+ you have arranged — `none`, `separate-account`, `sandbox` or `vm` — and it defaults to `none`,
102
+ because that is the truth on a machine where nobody has arranged anything. It is recorded as a
103
+ claim, never as a finding: a process cannot see the boundary it is inside. Beside the claim the
104
+ runner records what it *can* read — the account, and whether the process holds High or System
105
+ integrity — and says so when the two disagree. While `isolation` is `none`, an unattended run
106
+ will not start: nobody watching and nothing containing is the one combination this refuses to be.
107
+ Set it under **Settings → Execution → Where the bot runs**, and `cop doctor` and the System page
108
+ show the same assessment. A fresh install says `none`, so the unattended **Run sessions** button
109
+ is refused until you have arranged something and said so; **Step by step** is unaffected.
110
+
111
+ - **The chat cannot give this runner a file.** There is one kind of step, `command`, and nothing
112
+ the chat attaches is fetched, saved or run. This is not a setting that could be turned back on:
113
+ the step type does not exist and neither does the code that fetched one. A process that receives
114
+ a file and executes it is a loader whatever it meant by it, and that is precisely the shape a
115
+ security team opened an incident about. Work too long for one line is written **from a command
116
+ step** — `Set-Content` with a here-string — and run in the next, so its contents passed through a
117
+ step that was read and screened and it sits in the project where it can be read afterwards.
118
+ - **What comes back is data, never instructions.** The loop reads command output back into the
119
+ chat, and most output was not written by this machine: it is the contents of files, logs, commit
120
+ messages and pages that other people wrote. Any of it can carry text shaped like an order, and a
121
+ model that obeys it is doing what whoever edited that file wanted — which is the whole method of
122
+ an indirect prompt injection, since the attacker cannot reach the conversation and so leaves
123
+ something where it will be read. Both contracts forbid acting on it and require it to be reported
124
+ instead, and **every results file repeats the boundary above the first line of output**, because
125
+ a contract is sent once and the early turns of a long conversation fall out of what the model can
126
+ see. This is guidance to a model, not a control: it is the reason the allowlist, the refused
127
+ techniques and confirm mode exist underneath it.
128
+ - **Only the declared toolchain runs.** `execution.allowedPrograms` names the programs a command
129
+ may start — the shells, the JavaScript, .NET, Java, Python, Go and Rust tools, `git`, and a few
130
+ Windows utilities. A command that starts anything else is refused and sent back with the reason.
131
+ An empty list turns the gate off. This is a floor against the unknown binary, not a boundary:
132
+ allowing `node` allows `node -e`.
133
+ - **Autonomy is graded.** An unattended run carries strictly more restrictions than a watched one,
134
+ because the thing that makes a watched run safe is a person reading each line. Unattended
135
+ requires a non-empty allowlist, and refuses an allowed program used to evaluate a string
136
+ (`node -e`, `python -c`) or a shell wrapped in a shell (`cmd /c …`) — the forms that escape the
137
+ allowlist by construction.
138
+ - **An administrator can set a floor the operator cannot lower.** A `policy.lock.json` beside the
139
+ configuration may forbid unattended runs, cap the allowlist and add deny patterns. Every field
140
+ only ever tightens, and the runner never writes the file. A machine without one behaves exactly
141
+ as before.
142
+
143
+ ### Only the project folders
144
+
145
+ Every command a task runs — the implementer's steps and checks, the reviewer's steps and the checks
146
+ it derives — is held to the **project folders**: the session's own folder and every project
147
+ registered in Settings, so a test suite may still start the application from the repository next
148
+ to it. Refused before it runs:
149
+
150
+ - a path outside them — absolute, a network share, `..` that climbs out, a `cd` to the root — and
151
+ anything built from the operator's profile or the system: `~`, `$HOME`, `$env:USERPROFILE`,
152
+ `$env:APPDATA`, `$env:TEMP`, `%TEMP%` and the rest. **Reads as well as writes**: what a step prints
153
+ goes to the chat, so reading a key outside the project is sending it there;
154
+ - the registry and the other drives that are not files (`HKLM:`, `HKCU:`, `Cert:`);
155
+ - managing the computer: services, the network and firewall, local users, the clock, Windows
156
+ features, disks, machine-wide PowerShell modules;
157
+ - installs that land outside the project — `npm -g`, `dotnet tool -g`, `cargo install`,
158
+ `go install`, `pip install` into the machine's Python. The same tools installed into the project
159
+ (`npm install -D`, a local tool manifest, a `.venv`) stay open.
160
+
161
+ A check's own folder and the file a file check reads are held to the same folders, and a relative
162
+ file path is read from the project rather than from wherever the runner was started. A check the
163
+ reviewer derives that reaches outside is dropped rather than kept: the finding stands, the check
164
+ does not. Every run records the folders it was confined to in its `policy.json`.
165
+
166
+ **What this cannot do.** It reads what a step *says*, which is exactly the point — whatever the chat
167
+ proposes that reaches outside the project is not run. It cannot see what a program does once it is
168
+ running: `node build.js`, `npm run x` and a test suite can read and write anywhere the account can,
169
+ and so can a path assembled from variables at run time. That confinement is the operating system's
170
+ to give, and it is the real one:
171
+
172
+ 1. Create a **standard** (not administrator) local Windows account for the bot, and sign it in to
173
+ Copilot once with `cop login`.
174
+ 2. Give it modify rights on the project folders and nothing else:
175
+ `icacls "C:\Projects\my-app" /grant botuser:(OI)(CI)M`.
176
+ 3. **Check what it already has.** Folders created directly under `C:\` usually let every signed-in
177
+ user modify them by default, which would hand the bot every project there, not just yours to
178
+ give. Look with `icacls C:\Projects` and remove the broad grant where it should not be.
179
+ 4. Your own profile — `.ssh`, the browser, OneDrive — is private to you by default; confirm the bot
180
+ account cannot open `C:\Users\<you>`.
181
+ 5. Run the bot as that account, and set **Settings → Execution → Where the bot runs** to
182
+ *A separate low-privilege Windows account*.
183
+
184
+ Windows Sandbox, with only the project folders mapped in, gives the same guarantee with less to set
185
+ up and nothing left behind.
186
+
187
+ ### The local API is not open to everything on the machine
188
+
189
+ The API binds `127.0.0.1` only, and that was once taken to mean it needed no authentication. It
190
+ did. Two callers were always reachable: any page the operator has open in their ordinary browser,
191
+ which can issue requests to `127.0.0.1:4000` — CORS stops it reading the reply, which is no comfort
192
+ once a request has started a session — and any other process on the machine. So every request is
193
+ checked three ways before it reaches a route:
194
+
195
+ - the **`Host`** header must name this loopback port, which is what catches DNS rebinding — the
196
+ attacker's domain made to resolve to `127.0.0.1` is same-origin to the browser but still carries
197
+ its own name here;
198
+ - a present **`Origin`** must be the configured UI, since only a browser sets it and a page cannot
199
+ forge it;
200
+ - a per-install **token**, generated on first start into `data/api-token` (git-ignored), sent as
201
+ `Authorization: Bearer`, `x-cop-token`, or a `token` query parameter for the two cases that
202
+ cannot carry a header — `EventSource` and a plain `<a download>`.
203
+
204
+ `npm start` creates the token before either process starts and hands it to the web process only —
205
+ not to the API, whose environment command steps used to inherit, so a step never sees it; a step
206
+ that names the API's or the UI's port is refused. The web server binds `127.0.0.1` only. `GET /api/health` stays open so that "is it up yet" is still
207
+ answerable. To rotate the token, delete the file and restart.
208
+
209
+ The `data` folder — token, settings, sessions, the level-1 contract — is narrowed at start to the
210
+ operator's account and SYSTEM, and the API refuses to start while any other account can open it:
211
+ under `C:\Projects` a folder otherwise inherits *Authenticated Users: Modify*, which is every
212
+ account on the machine. A step never sees the token (see above) and is refused for naming the API's
213
+ ports or the data folder.
214
+
215
+ This does not contain a process already running as the operator — nothing at this layer can, since
216
+ that process can read the file. It moves the API from "anything on this machine, and several things
217
+ off it" to "the operator's own account, through a browser that was handed the key".
218
+
219
+ Every task writes `policy.json` into its run folder and a `POLICY` block into its log: the mode,
220
+ the allowlist and its digest, that the chat cannot supply files, digests of the deny list and of the
221
+ built-in refusals, whether a lock was in force and what it changed, and the account the run used.
222
+ So "what was this permitted to do at the time" is answered from the run folder rather than from a
223
+ configuration file that has been edited since.
224
+
225
+ ### Before a company deploys it
226
+
227
+ A compliance review against the common frameworks (CIS Controls v8, NIST SP 800-53, ISO/IEC
228
+ 27001 Annex A, the Microsoft baselines) was run on 2026-09-27. Nothing in the software is a
229
+ hidden mechanism; what follows is what a company has to decide, approve or configure before it
230
+ runs this on managed laptops, because no code change can decide it for them.
231
+
232
+ **Approvals to obtain.**
233
+
234
+ - *Identity and licensing.* The bot drives a licensed person's Microsoft 365 Copilot through
235
+ Edge, not through the API. Microsoft documents the API as the supported way to automate
236
+ Copilot; driving the web client is not documented as supported, and a per-user licence is
237
+ not meant to be shared or driven by a service identity. Ask the Microsoft contact whether the
238
+ intended use is acceptable under the licence, and decide which person's identity signs in —
239
+ one licensed identity per operator, never a shared one. Every prompt, upload and Purview
240
+ record is attributed to that identity.
241
+ - *Conditional Access.* The bot's Edge runs a profile of its own under
242
+ `%LOCALAPPDATA%\copilot-operator`, started by `node.exe` with Playwright's debugging channel.
243
+ A tenant's sign-in frequency, compliant-device and managed-browser policies may refuse it or
244
+ pause it for sign-in; and an Edge policy that forbids remote debugging or forces extensions
245
+ stops it outright. Test on one managed device before rolling out; where the baseline cannot
246
+ be relaxed, run it in a VM or Windows Sandbox outside the baseline's scope.
247
+ - *Data protection.* See "Where the reports go" above. Where the law requires it, record the
248
+ processing (a DPIA where automated tooling that stores every command's output and sends it to
249
+ an AI service triggers one; works-council consultation where tooling that records employees'
250
+ actions requires it). Mirrored source may carry personal data in fixtures and exports.
251
+ - *AI-use policy.* Commands written by a language model execute on the endpoint. Confirm mode
252
+ keeps a person on every line; unattended mode removes that person and is the decision most
253
+ AI-governance policies want recorded. `policy.lock.json` can forbid it machine-wide.
254
+ - *Security operations.* Tell the SOC what to expect: `node.exe` starting PowerShell (every
255
+ step, as `-Command` text, which PowerShell logging records verbatim) and `taskkill`; Edge
256
+ started by `node.exe` over a debugging pipe; updates pulled from a git remote. Register the
257
+ install so the process chain is known rather than investigated.
258
+
259
+ **Configuration to set** — as an administrator, in `%ProgramData%\copilot-operator\policy.lock.json`,
260
+ which the operator cannot edit (the same file beside the install is honoured too):
261
+
262
+ ```json
263
+ {
264
+ "maxMode": "confirm",
265
+ "requireIsolation": true,
266
+ "allowDesktopMirror": false,
267
+ "allowEnvFiles": false,
268
+ "passEnv": [],
269
+ "allowedPrograms": ["pwsh", "powershell", "cmd", "node", "npm", "npx", "git", "dotnet"],
270
+ "update": { "requireSigned": true, "remote": "https://git.example.com/tools/copilot-operator.git" }
271
+ }
272
+ ```
273
+
274
+ Every field only tightens; `update` makes `npm run update` insist on signed commits and refuse
275
+ any other remote. Keep `data/` and `runs/` on a local disk (`COP_DATA_DIR`, `runsDir`), set
276
+ `runsRetentionDays`, and fork the repository internally so updates come from a remote the
277
+ company controls.
278
+
279
+ **Keeping it patched.** Dependencies are pinned in `package-lock.json`; `npm audit` reports
280
+ known vulnerabilities in them, and `npm run update` brings in whatever the remote has fixed.
281
+ Node.js and Edge are patched by the machine's own update process. Nothing here watches for
282
+ end-of-support runtimes — that is the deploying company's asset management.
283
+
284
+ **Stopping it.** "Stop" on a session cuts in after the current step and ends the task
285
+ `aborted`; "Pause" lets the task in flight finish and holds the queue. Ctrl+C in the `npm start`
286
+ terminal stops the API and the UI; processes a step started are stopped with the task (asked
287
+ first, forced after a few seconds), and the run log names any it could not. To end the bot's
288
+ access: `cop logout` signs its profile out, deleting `data/api-token` and restarting rotates the
289
+ API key, `cop login --fresh` deletes the profile.
290
+
291
+ **Where it does not fit.** On a fleet that enforces PowerShell Constrained Language Mode the
292
+ bot's own probes degrade gracefully (permissions are read through `Get-Acl`; elevation reads as
293
+ unknown), but the model's steps themselves run constrained, and most builds will not. That is a
294
+ fleet to run the bot outside of, in a VM, not one to request exceptions on.
295
+
296
+ ### What an update trusts
297
+
298
+ `npm run update` fetches commits, fast-forwards onto them, installs what the lockfile names and
299
+ builds the result — and that result is the thing that runs commands on this machine. Its trust
300
+ boundary is one sentence: **whoever controls the remote controls this bot.** Three things make that
301
+ checkable rather than implied.
302
+
303
+ - **The remote is pinned.** The address a checkout last updated from is recorded in
304
+ `data/update-remote`. If `origin` now points somewhere else the update stops before anything is
305
+ fetched, prints both addresses, and asks. Trust on first use — the first update records what is
306
+ there. Accept a deliberate move with `npm run update -- --accept-remote`.
307
+ - **Signatures, if your fork has them.** `npm run update -- --require-signed` refuses a HEAD that
308
+ git cannot verify, and tells an unsigned commit apart from a bad signature. Off by default,
309
+ because this project does not sign its commits and a check that always fails is a check that gets
310
+ removed; it is here so an organisation whose fork *is* signed can make it mean something.
311
+ - **Every update is written down.** A line in `data/update-log.jsonl`: when, from which remote,
312
+ which commit to which, how many came in, and whether a signature was demanded. `data/` is
313
+ git-ignored, so the record survives the pull it describes.
314
+
315
+ None of this verifies what the code *does*. A signed commit from a compromised maintainer is a
316
+ signed commit. It narrows "anything that can reach the network" to "whoever holds the remote this
317
+ checkout was installed from", and says so out loud.
318
+
319
+ **The honest limit.** Everything above except the first rule is pattern matching and configuration.
320
+ It raises the cost of the ordinary accident and the ordinary injected instruction; it is not a
321
+ security boundary, and a determined attacker with a language model to write for them will find a
322
+ phrasing none of it anticipated. The boundary is the first rule — a separate low-privilege Windows
323
+ account, Windows Sandbox or a VM — and the runner can record whether you have arranged one, refuse
324
+ to run unwatched until you have, and never do it for you.
325
+
326
+ ## Requirements
327
+
328
+ - Windows 10 or 11
329
+ - Node.js 20 or newer
330
+ - Microsoft Edge, already installed
331
+ - A Microsoft 365 Copilot account, signed in once by the user
332
+
333
+ ## Installing it in a project
334
+
335
+ ```bash
336
+ npm install --save-dev copilot-operator
337
+ npx cop login --account you@tenant.org # sign in once; the Edge profile is reused across projects
338
+ npx cop start --open # the API and the interface, on http://127.0.0.1:4000/
339
+ ```
340
+
341
+ Installed this way, the bot belongs to the project it is installed in. Its records — settings,
342
+ sessions, the API key, every run's logs — go to `.copilot-operator/` in the project, which carries
343
+ its own `.gitignore`, so the project's git never sees it and the bot's version control finds a
344
+ clean tree. The interface ships prebuilt and is served by the same process on one port; there is no
345
+ second server and nothing is compiled on the machine. `npx cop start --port 4100` picks another
346
+ port. `npm update copilot-operator` updates it; `npm run update` is for a clone.
347
+
348
+ The bot is then inside the folders its own steps may write, so a step that reaches for
349
+ `node_modules/copilot-operator`, for `.copilot-operator`, or that installs, updates or removes the
350
+ bot as a package is refused. What no rule can see is a step that rewrites the project's
351
+ `package.json` with a different version of the bot for a later `npm install` to fetch; the commit
352
+ the bot makes after the task shows such a change, and it is worth a look before the next install.
353
+
354
+ ## Releasing a new version
355
+
356
+ Publishing is done by GitHub Actions (`.github/workflows/publish.yml`), not from a laptop:
357
+
358
+ ```bash
359
+ npm version patch # or minor / major: updates package.json, commits, tags vX.Y.Z
360
+ git push --follow-tags # the tag starts the workflow: typecheck, checks, build, publish
361
+ ```
362
+
363
+ The workflow publishes with npm's Trusted Publishing — no npm token is stored anywhere; npm accepts
364
+ the package because it comes from this repository's `publish.yml` — and with provenance, so the
365
+ package page on npm says which commit and which run built it. Set up once, after the first manual
366
+ `npm publish`: on npmjs.com, the package's **Settings → Trusted Publisher → GitHub Actions**, with
367
+ repository `ivan-markov-666/copilot-operator` and workflow `publish.yml`. A tag whose version does
368
+ not match `package.json` stops the workflow before anything is built.
369
+
370
+ ## Working on it from a clone
371
+
372
+ ```bash
373
+ npm install # root and the web workspace
374
+ npx tsx src/cli.ts login --account you@tenant.org # sign in once; the Edge profile is reused
375
+ npx tsx src/cli.ts doctor # check the machine
376
+ ```
377
+
378
+ The bot never types credentials. `login` opens Edge, waits for the chat to appear, checks
379
+ that the right account is signed in, and stores nothing but the browser profile.
380
+
381
+ ### The web UI
382
+
383
+ ```bash
384
+ npm start # builds the API, then runs it and the UI together; Ctrl+C stops both
385
+ ```
386
+
387
+ Then open http://localhost:3210. `npm run dev` does the same and opens the browser for you.
388
+ The two can also run separately: `npm run api` (NestJS on 127.0.0.1:4000) and `npm run web`
389
+ (Next.js on localhost:3210).
390
+
391
+ Create a session, add tasks with their level 2 instructions, press Run, approve each step
392
+ from the page, read the summary when it finishes. See [`docs/ui.md`](docs/ui.md).
393
+
394
+ ### Removing it from a machine
395
+
396
+ Nothing is installed system-wide, nothing runs at start-up, no service and no scheduled task
397
+ exist. What the software leaves, and where:
398
+
399
+ | What | Where | Remove |
400
+ |---|---|---|
401
+ | settings, sessions, the API token, level-1 and context texts | `data/` in the checkout | delete the folder |
402
+ | every run's logs, reports and failure screenshots | `runs/` in the checkout | delete the folder (or set `runsRetentionDays`) |
403
+ | backups taken by `npm run update` | `data-backups/` in the checkout | delete the folder |
404
+ | the bot's own Edge profile, with its Microsoft 365 session | `%LOCALAPPDATA%\copilot-operator\edge-profile` | sign out in that window (`cop login --fresh` deletes it) and delete the folder |
405
+ | the API key the browser was handed | the browser's storage for `localhost:3210` | clear site data for that origin |
406
+ | logs saved on request, and the optional project mirror | `Desktop\copilot-operator-logs`, the mirror folder in Settings | delete them; they may already be in OneDrive |
407
+ | the reports uploaded to Copilot | the account's OneDrive and Copilot chat history | delete the conversations, or let the tenant's retention run |
408
+ | a machine-wide policy lock, if an administrator placed one | `%ProgramData%\copilot-operator\policy.lock.json` | the administrator removes it |
409
+
410
+ The permissions the API set on `data/` and `runs/` (owner and SYSTEM only) go with the folders.
411
+
412
+ ### Updating a machine that has been used
413
+
414
+ ```bash
415
+ npm run update # back up, pull, install, build
416
+ npm run update -- --check # say what would happen, change nothing
417
+ ```
418
+
419
+ **A pull cannot lose what you have entered.** Everything this program records lives in `data/`
420
+ (settings, sessions, level 2 presets, a customised level 1, the organisation and work texts, the
421
+ model list) and `runs/` (every log, report and artifact of every task). Both are in `.gitignore`,
422
+ every write in `src/` goes to one of them or to the Desktop, and git does not know they exist.
423
+
424
+ What a pull *can* do is refuse to start. `npm run update` deals with each reason in turn: it
425
+ copies `data/` to `data-backups/<timestamp>/` before touching anything, refuses to run while the
426
+ bot is (swapping the code under a run in flight is the one thing here that could really break
427
+ something), puts any local edits to tracked files into a named stash and tells you the command to
428
+ get them back, pulls **fast-forward only** so no merge is ever made on your behalf, and then
429
+ reinstalls and rebuilds — because a `dist/` older than its `src/` runs the previous version and
430
+ reports the previous version's bugs.
431
+
432
+ Run it with `--check` first if you want to see what it would do. When nothing has come in it
433
+ installs and builds nothing; `--rebuild` forces both. It uses `npm ci`, and only when the pull
434
+ actually moved a dependency: `npm install` may rewrite `package-lock.json`, and a rewritten
435
+ lockfile is a tracked file that differs from the commit — which is one of the phantom local
436
+ changes this is here to stop.
437
+
438
+ **`npm warn allow-scripts ... esbuild`** on a machine whose npm requires install scripts to be
439
+ approved is expected and usually harmless. esbuild gets its Windows binary from an optional
440
+ dependency (`@esbuild/win32-x64`), which installs without any script; the blocked `postinstall`
441
+ only revalidates it. Check with `npx tsx --version` — if that answers, `npm run check` will run.
442
+ If it does not, `npm approve-scripts esbuild` and install again.
443
+
444
+ If files looked modified that you never edited, that was line endings: two Windows machines, one
445
+ with `core.autocrlf` set and one without. `.gitattributes` settles it.
446
+
447
+ ### The terminal, for one task
448
+
449
+ ```bash
450
+ cp run.example.yaml run.yaml # then edit it
451
+ npx tsx src/cli.ts run run.yaml
452
+ ```
453
+
454
+ ### Commands
455
+
456
+ | Command | What it does |
457
+ |---|---|
458
+ | `cop login` | opens Edge with the bot profile and waits for you to sign in |
459
+ | `cop doctor [run.yaml]` | checks Node, Edge, PowerShell, the Desktop, the config |
460
+ | `cop dirs <projectRoot>` | lists the directories you can select for the mirror |
461
+ | `cop mirror <run.yaml>` | refreshes the Desktop folder without touching the chat |
462
+ | `cop run <run.yaml>` | one task as a new session; `--unattended` skips the per-step prompt |
463
+ | `cop chat [run.yaml]` | prints the last run's conversation name and link |
464
+
465
+ Before it is built, run them as `npx tsx src/cli.ts <command>`.
466
+
467
+ **Testing it for the first time:** follow [`docs/testing.md`](docs/testing.md). Nine steps,
468
+ ordered so the risk climbs slowly, starting with checks that need no account at all.
469
+ `run.smoke.yaml` is a ready-made read-only first run.
470
+
471
+ ## How it works
472
+
473
+ | Piece | File | State |
474
+ |---|---|---|
475
+ | DOM locators for the Copilot web app | `src/transport/locators.ts` | captured from the live app |
476
+ | Command runner, dual timeouts, process-tree kill | `src/exec/runner.ts` | implemented, checked |
477
+ | Chat naming and reattach after re-login | `src/transport/chatSession.ts` | implemented, checked |
478
+ | Covering message for the results file | `src/protocol/reporter.ts` | implemented, checked |
479
+ | Desktop folder resolution and guard rails | `src/context/contextFiles.ts` | implemented, checked |
480
+ | Project mirror, flattened names, incremental | `src/context/projectMirror.ts` | implemented, checked |
481
+ | Pacing, send cap, backoff | `src/util/pacing.ts` | implemented, checked |
482
+ | Playwright transport | `src/transport/copilotTransport.ts` | implemented; selectors verified live |
483
+ | Reply parser and the JSON contract | `src/protocol/parser.ts`, `replySchema.ts` | implemented, checked |
484
+ | Report file, splitting, redaction | `src/exec/reportFile.ts` | implemented, checked |
485
+ | Deny list and the confirm gate | `src/exec/policy.ts` | implemented, checked |
486
+ | Config schema and loader | `src/config/schema.ts` | implemented |
487
+ | The loop, per task and per session | `src/orchestrator/taskRunner.ts` | run live end to end |
488
+ | Sessions, tasks, level 2 presets on disk | `src/session/` | implemented, checked |
489
+ | API | `src/api/` (NestJS) | implemented, exercised over HTTP |
490
+ | Web UI | `web/` (Next.js) | implemented |
491
+ | CLI | `src/cli.ts` | implemented |
492
+
493
+ Design and findings:
494
+
495
+ - [`docs/ui.md`](docs/ui.md) — the web UI, the API and the session model
496
+ - [`docs/testing.md`](docs/testing.md) — how to test it the first time, step by step
497
+ - [`docs/architecture.md`](docs/architecture.md) — the whole design
498
+ - [`docs/locators-findings.md`](docs/locators-findings.md) — what the live Copilot DOM looks like and why
499
+ - [`docs/tech-stack-research.md`](docs/tech-stack-research.md) — why Playwright and not the alternatives
500
+ - [`docs/deferred.md`](docs/deferred.md) — designed, argued and deliberately not built yet, with the reasoning kept
501
+
502
+ ### What the planning persona knows
503
+
504
+ The brief the UI hands to a chat model — the Kerrigan persona — carries the names of this
505
+ application's own screens and controls, so it can tell the operator to press "Create the sessions
506
+ and tasks" rather than "import the plan". They live in `src/plan/systemGuide.ts` and are embedded
507
+ in the brief by `src/plan/brief.ts`, and `test/guide.check.ts` pins every one of them to
508
+ `web/lib/strings.ts` in both languages. Renaming a control in the interface therefore breaks that
509
+ check by name, and a new control the operator would ever be told to press has to be added to the
510
+ guide as well, or the persona does not know it exists. The same check reads the pages themselves,
511
+ so a label that is still in the dictionary but that nothing renders any more fails too, and it
512
+ holds the labels the brief quotes in its own prose to the same standard.
513
+
514
+ ## Notable findings
515
+
516
+ These cost real investigation and are worth knowing before touching the code.
517
+
518
+ - **Code blocks cannot be read from the DOM.** They are virtualized and interleave line
519
+ numbers with the code. A valid JSON reply came back from `innerText` with a chunk missing.
520
+ The message-level "Copy Response" button plus clipboard access is the only safe read.
521
+ - **Downloads are plain anchors** with a `blob:` href and a `download` attribute, which is
522
+ why nothing useful appears on hover. Playwright's download event handles them.
523
+ - **Enter does not send.** The composer is a contenteditable span; the Send button must be
524
+ clicked.
525
+ - **A message cannot be only an attachment.** Text is mandatory, so the covering message is
526
+ part of the protocol rather than decoration.
527
+ - **Uploads go through the user's OneDrive.** The chat says so, and the attachment id carries
528
+ an `SPO_` prefix. That prefix is also the most reliable "upload finished" signal.
529
+ - **Copilot destroys `[name]:` in its own output.** `[math]::Round($x, 2)` reaches the
530
+ runner as `:Round($x, 2)`, even inside a fenced code block, because `[label]:` is markdown
531
+ for a link reference. `[pscustomobject]` and `[double]$x` survive, since the damage needs
532
+ the colon. The prompts forbid that form and the runner refuses a command that arrives
533
+ wrecked rather than executing something nobody wrote.
534
+ - **`lastChatMessage` is not the whole answer.** It is the answer body; the copy button is
535
+ its sibling. A wait built on `[data-testid="lastChatMessage"] [data-testid="CopyButtonTestId"]`
536
+ never completes. The anchor is the last `copilot-message-div`.
537
+
538
+ ## Checks
539
+
540
+ ```bash
541
+ npm run check # all of them
542
+ ```
543
+
544
+ Each one prints what it exercised rather than asserting silently, so a failure is readable.
545
+ There is no live-tenant test: that is the part that needs a human and a signed-in browser.
546
+
547
+ ## Related
548
+
549
+ [context-picker](https://github.com/ivan-markov-666/context-picker) (MIT) — the same author's
550
+ VS Code and Visual Studio extension for picking which project files an LLM should see. This
551
+ project follows its file-naming convention, so the two produce interchangeable folders, and
552
+ can drive it directly for per-file selection.
553
+
554
+ ## Licence
555
+
556
+ MIT. See [`LICENSE`](LICENSE).
@@ -0,0 +1,18 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from '@nestjs/common';
8
+ import { OperatorController } from './operator.controller.js';
9
+ import { OperatorService } from './operator.service.js';
10
+ let AppModule = class AppModule {
11
+ };
12
+ AppModule = __decorate([
13
+ Module({
14
+ controllers: [OperatorController],
15
+ providers: [OperatorService],
16
+ })
17
+ ], AppModule);
18
+ export { AppModule };