@junghanacs/entwurf 0.14.2 → 0.15.1
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.
- package/AGENTS.md +9 -5
- package/CHANGELOG.md +149 -0
- package/DELIVERY.md +72 -59
- package/README.md +120 -20
- package/VERIFY.md +10 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +2 -2
- package/docs/external-mcp-host.md +26 -4
- package/docs/setup-clean-host.md +118 -18
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
- package/mcp/entwurf-bridge/src/index.ts +10 -6
- package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
- package/package.json +22 -11
- package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
- package/pi-extensions/entwurf-control.ts +17 -8
- package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
- package/pi-extensions/lib/meta-sender-identity.ts +10 -2
- package/pi-extensions/lib/meta-session.ts +102 -29
- package/pi-extensions/lib/mux-fresh-call.ts +82 -11
- package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
- package/run.sh +1079 -186
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +1174 -0
- package/scripts/check-copilot-launch.ts +395 -0
- package/scripts/check-copilot-receive-arm.ts +734 -0
- package/scripts/check-copilot-statusline.ts +122 -0
- package/scripts/check-entwurf-capabilities.ts +25 -6
- package/scripts/check-fresh-cut-gate.sh +104 -27
- package/scripts/check-gate-qualification.ts +10 -1
- package/scripts/check-install-container.sh +2 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-meta-capability-source.ts +89 -5
- package/scripts/check-meta-doctor-oracle.sh +26 -0
- package/scripts/check-meta-manifest-schema.py +38 -0
- package/scripts/check-meta-receiver-marker.ts +25 -2
- package/scripts/check-meta-session.ts +32 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +242 -0
- package/scripts/copilot-bridge-install.sh +384 -0
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/copilot-launch.sh +236 -0
- package/scripts/copilot-mcp-bridge.sh +177 -0
- package/scripts/copilot-mcp-config.py +221 -0
- package/scripts/copilot-receive-bridge.sh +483 -0
- package/scripts/copilot-statusline-bridge.sh +149 -0
- package/scripts/copilot-statusline-config.py +201 -0
- package/scripts/copilot-statusline.sh +68 -0
- package/scripts/dev-bin.sh +53 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/meta-bridge-hook-log.sh +17 -5
- package/scripts/mutants/capability-cache.json +64 -0
- package/scripts/mutants/copilot-birth.json +244 -0
- package/scripts/mutants/copilot-launch.json +187 -0
- package/scripts/mutants/copilot-receive.json +263 -0
- package/scripts/mutants/fresh-cut.json +17 -0
- package/scripts/mutants/mux-fresh-call.json +216 -2
- package/scripts/mutants/pack-install.json +17 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-async-delivery/README.md +170 -125
- package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
- package/scripts/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- package/scripts/smoke-copilot-mcp-state.sh +153 -0
- package/scripts/smoke-copilot-statusline-state.sh +131 -0
- package/scripts/smoke-mux-fresh-call-live.ts +2 -0
- package/scripts/smoke-mux-lifecycle-live.ts +3 -1
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stateful Copilot MCP-config adapter; stdlib only.
|
|
3
|
+
|
|
4
|
+
Owns ONE server key (``entwurf-bridge``) inside ``~/.copilot/mcp-config.json``.
|
|
5
|
+
The file wrapper is always ``mcpServers``. The stdio entry follows Copilot CLI's
|
|
6
|
+
writer, not the API wire schema: ``{type:"local", command, args?}``.
|
|
7
|
+
Adopts regular files, refuses symlinks, records first-install preimages.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
|
|
16
|
+
STATE_SCHEMA_VERSION = 1
|
|
17
|
+
SERVER_KEY = "entwurf-bridge"
|
|
18
|
+
ENTRY_TYPE = "local"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def die(code: int, message: str) -> "None":
|
|
22
|
+
sys.stderr.write(message.rstrip("\n") + "\n")
|
|
23
|
+
raise SystemExit(code)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_object(path: str, label: str) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
29
|
+
raw = handle.read()
|
|
30
|
+
except OSError as error:
|
|
31
|
+
die(4, f"copilot-mcp: cannot read {label} {path}: {error}")
|
|
32
|
+
if not raw.strip():
|
|
33
|
+
return {}
|
|
34
|
+
try:
|
|
35
|
+
value = json.loads(raw)
|
|
36
|
+
except json.JSONDecodeError as error:
|
|
37
|
+
die(4, f"copilot-mcp: {label} {path} is not valid JSON: {error}")
|
|
38
|
+
if not isinstance(value, dict):
|
|
39
|
+
die(4, f"copilot-mcp: {label} {path} top-level must be a JSON object")
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def atomic_write(path: str, value: dict) -> None:
|
|
44
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
45
|
+
os.makedirs(directory, exist_ok=True)
|
|
46
|
+
fd, temporary = tempfile.mkstemp(dir=directory, prefix=".copilot-mcp-", suffix=".json")
|
|
47
|
+
try:
|
|
48
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
49
|
+
json.dump(value, handle, indent=2)
|
|
50
|
+
handle.write("\n")
|
|
51
|
+
os.replace(temporary, path)
|
|
52
|
+
except BaseException:
|
|
53
|
+
try:
|
|
54
|
+
os.unlink(temporary)
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
pass
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def now() -> str:
|
|
61
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def our_entry(command: str) -> dict:
|
|
65
|
+
# CLI `copilot mcp add` writes type:"local" (not "stdio") plus command/args.
|
|
66
|
+
# args is present and empty so the inverse has a stable shape; env/timeout/tools stay omitted.
|
|
67
|
+
return {"type": ENTRY_TYPE, "command": command, "args": []}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def checked_state(state_path: str) -> dict:
|
|
71
|
+
state = load_object(state_path, "install-state")
|
|
72
|
+
required = (
|
|
73
|
+
"managedConfigPath",
|
|
74
|
+
"serverKey",
|
|
75
|
+
"command",
|
|
76
|
+
"detectMode",
|
|
77
|
+
"configExistedBefore",
|
|
78
|
+
"preimage",
|
|
79
|
+
)
|
|
80
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or any(key not in state for key in required):
|
|
81
|
+
die(4, f"copilot-mcp: install-state {state_path} has an unsupported shape")
|
|
82
|
+
if state.get("serverKey") != SERVER_KEY:
|
|
83
|
+
die(4, f"copilot-mcp: install-state {state_path} serverKey is not {SERVER_KEY}")
|
|
84
|
+
return state
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def prior_state(state_path: str, config_path: str) -> dict | None:
|
|
88
|
+
if not os.path.exists(state_path):
|
|
89
|
+
return None
|
|
90
|
+
state = checked_state(state_path)
|
|
91
|
+
return state if state["managedConfigPath"] == os.path.abspath(config_path) else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def install(config_path: str, command: str, state_path: str) -> None:
|
|
95
|
+
if os.path.islink(config_path):
|
|
96
|
+
die(3, f"copilot-mcp: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
|
|
97
|
+
|
|
98
|
+
existed = os.path.exists(config_path)
|
|
99
|
+
data = load_object(config_path, "config") if existed else {}
|
|
100
|
+
servers = data.get("mcpServers")
|
|
101
|
+
if servers is None:
|
|
102
|
+
servers = {}
|
|
103
|
+
data["mcpServers"] = servers
|
|
104
|
+
if not isinstance(servers, dict):
|
|
105
|
+
die(4, f"copilot-mcp: {config_path} mcpServers must be a JSON object")
|
|
106
|
+
|
|
107
|
+
prior = prior_state(state_path, config_path)
|
|
108
|
+
if prior is None:
|
|
109
|
+
state = {
|
|
110
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
111
|
+
"managedConfigPath": os.path.abspath(config_path),
|
|
112
|
+
"serverKey": SERVER_KEY,
|
|
113
|
+
"command": command,
|
|
114
|
+
"detectMode": "adopt-regular-file" if existed else "created-new",
|
|
115
|
+
"configExistedBefore": existed,
|
|
116
|
+
"preimage": servers.get(SERVER_KEY),
|
|
117
|
+
"installedAt": now(),
|
|
118
|
+
}
|
|
119
|
+
else:
|
|
120
|
+
state = prior
|
|
121
|
+
|
|
122
|
+
servers[SERVER_KEY] = our_entry(command)
|
|
123
|
+
atomic_write(config_path, data)
|
|
124
|
+
atomic_write(state_path, state)
|
|
125
|
+
sys.stdout.write(f"{state['detectMode']} {os.path.abspath(config_path)}\n")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def uninstall(state_path: str) -> None:
|
|
129
|
+
if not os.path.exists(state_path):
|
|
130
|
+
die(2, f"copilot-mcp: no install-state at {state_path} — nothing to undo")
|
|
131
|
+
state = checked_state(state_path)
|
|
132
|
+
config_path = state.get("managedConfigPath")
|
|
133
|
+
if not isinstance(config_path, str) or not os.path.isabs(config_path):
|
|
134
|
+
die(4, f"copilot-mcp: install-state {state_path} has no absolute managedConfigPath")
|
|
135
|
+
if os.path.islink(config_path):
|
|
136
|
+
die(3, f"copilot-mcp: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
|
|
137
|
+
|
|
138
|
+
if os.path.exists(config_path):
|
|
139
|
+
data = load_object(config_path, "config")
|
|
140
|
+
servers = data.get("mcpServers")
|
|
141
|
+
if isinstance(servers, dict):
|
|
142
|
+
preimage = state.get("preimage")
|
|
143
|
+
if preimage is None:
|
|
144
|
+
servers.pop(SERVER_KEY, None)
|
|
145
|
+
else:
|
|
146
|
+
servers[SERVER_KEY] = preimage
|
|
147
|
+
created = state.get("detectMode") == "created-new" and state.get("configExistedBefore") is False
|
|
148
|
+
only_ours = created and len(servers) == 0 and set(data.keys()) == {"mcpServers"}
|
|
149
|
+
if only_ours:
|
|
150
|
+
os.remove(config_path)
|
|
151
|
+
else:
|
|
152
|
+
atomic_write(config_path, data)
|
|
153
|
+
|
|
154
|
+
os.remove(state_path)
|
|
155
|
+
sys.stdout.write(f"uninstalled {config_path}\n")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _doctor_invocation(config_path: str, command: str):
|
|
159
|
+
if os.path.islink(config_path):
|
|
160
|
+
return "symlink", None
|
|
161
|
+
if not os.path.exists(config_path):
|
|
162
|
+
return "file-absent", None
|
|
163
|
+
try:
|
|
164
|
+
data = load_object(config_path, "config")
|
|
165
|
+
except SystemExit:
|
|
166
|
+
return "invalid-json", None
|
|
167
|
+
servers = data.get("mcpServers")
|
|
168
|
+
if not isinstance(servers, dict) or SERVER_KEY not in servers:
|
|
169
|
+
return "not-ours", None
|
|
170
|
+
server = servers.get(SERVER_KEY)
|
|
171
|
+
if not isinstance(server, dict):
|
|
172
|
+
return "invalid-entry", None
|
|
173
|
+
found = server.get("command")
|
|
174
|
+
args = server.get("args", [])
|
|
175
|
+
env = server.get("env", {})
|
|
176
|
+
if server.get("type") != ENTRY_TYPE:
|
|
177
|
+
return "invalid-entry", None
|
|
178
|
+
if not isinstance(found, str) or not found:
|
|
179
|
+
return "invalid-entry", None
|
|
180
|
+
if found != command:
|
|
181
|
+
return "not-ours", None
|
|
182
|
+
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
|
|
183
|
+
return "invalid-entry", None
|
|
184
|
+
if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
|
|
185
|
+
return "invalid-entry", None
|
|
186
|
+
return "configured", {"command": found, "args": args, "env": env}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def doctor_static(config_path: str, command: str) -> None:
|
|
190
|
+
status, invocation = _doctor_invocation(config_path, command)
|
|
191
|
+
if status != "configured":
|
|
192
|
+
sys.stdout.write(f"{status}\n")
|
|
193
|
+
return
|
|
194
|
+
sys.stdout.write(f"configured {invocation['command']}\n")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def doctor_invocation(config_path: str, command: str) -> None:
|
|
198
|
+
status, invocation = _doctor_invocation(config_path, command)
|
|
199
|
+
if status != "configured":
|
|
200
|
+
die(4, f"copilot-mcp: cannot read configured invocation from {config_path}: {status}")
|
|
201
|
+
sys.stdout.write(json.dumps(invocation, separators=(",", ":")) + "\n")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(argv: list[str]) -> None:
|
|
205
|
+
if len(argv) < 2:
|
|
206
|
+
die(5, "usage: copilot-mcp-config.py <install|uninstall|doctor-static|doctor-invocation> ...")
|
|
207
|
+
match argv[1]:
|
|
208
|
+
case "install" if len(argv) == 5:
|
|
209
|
+
install(argv[2], argv[3], argv[4])
|
|
210
|
+
case "uninstall" if len(argv) == 3:
|
|
211
|
+
uninstall(argv[2])
|
|
212
|
+
case "doctor-static" if len(argv) == 4:
|
|
213
|
+
doctor_static(argv[2], argv[3])
|
|
214
|
+
case "doctor-invocation" if len(argv) == 4:
|
|
215
|
+
doctor_invocation(argv[2], argv[3])
|
|
216
|
+
case _:
|
|
217
|
+
die(5, "usage: copilot-mcp-config.py <install config command state|uninstall state|doctor-static config command|doctor-invocation config command>")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main(sys.argv)
|