server-handbook 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ """Server Handbook: durable server context and managed deployment records."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,391 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:server-handbook:service:1",
4
+ "title": "Server Handbook service declaration v1",
5
+ "description": "Portable structural validation. The runtime also checks native absolute paths, configured roots, symlinks, source placement, file contents and HTTP URL semantics.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version",
10
+ "id",
11
+ "name",
12
+ "kind",
13
+ "state",
14
+ "home",
15
+ "readme"
16
+ ],
17
+ "properties": {
18
+ "schema_version": {
19
+ "type": "integer",
20
+ "const": 1
21
+ },
22
+ "id": {
23
+ "$ref": "#/$defs/serviceId"
24
+ },
25
+ "name": {
26
+ "$ref": "#/$defs/shortText"
27
+ },
28
+ "kind": {
29
+ "enum": [
30
+ "compose",
31
+ "systemd",
32
+ "project",
33
+ "desktop",
34
+ "toolchain"
35
+ ]
36
+ },
37
+ "state": {
38
+ "enum": [
39
+ "prepared",
40
+ "installed",
41
+ "deployed",
42
+ "retired"
43
+ ]
44
+ },
45
+ "home": {
46
+ "$ref": "#/$defs/absolutePath"
47
+ },
48
+ "readme": {
49
+ "allOf": [
50
+ {
51
+ "$ref": "#/$defs/absolutePath"
52
+ },
53
+ {
54
+ "pattern": "\\.[mM][dD]$"
55
+ }
56
+ ]
57
+ },
58
+ "version": {
59
+ "$ref": "#/$defs/shortText"
60
+ },
61
+ "systemd_units": {
62
+ "type": "array",
63
+ "maxItems": 256,
64
+ "items": {
65
+ "type": "string",
66
+ "maxLength": 256,
67
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9@_.:\\\\-]*\\.(service|socket|timer|target|mount|automount|path|slice|scope)$",
68
+ "not": {
69
+ "pattern": "\\s"
70
+ }
71
+ }
72
+ },
73
+ "compose": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": [
77
+ "project",
78
+ "service",
79
+ "files"
80
+ ],
81
+ "properties": {
82
+ "project": {
83
+ "type": "string",
84
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,62}$",
85
+ "not": {
86
+ "pattern": "\\s"
87
+ }
88
+ },
89
+ "service": {
90
+ "type": "string",
91
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$",
92
+ "not": {
93
+ "pattern": "\\s"
94
+ }
95
+ },
96
+ "files": {
97
+ "type": "array",
98
+ "minItems": 1,
99
+ "maxItems": 32,
100
+ "items": {
101
+ "$ref": "#/$defs/absolutePath"
102
+ }
103
+ }
104
+ }
105
+ },
106
+ "packages": {
107
+ "$ref": "#/$defs/textArray"
108
+ },
109
+ "local_ports": {
110
+ "type": "array",
111
+ "maxItems": 256,
112
+ "items": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "required": [
116
+ "bind",
117
+ "port",
118
+ "transport",
119
+ "purpose"
120
+ ],
121
+ "properties": {
122
+ "bind": {
123
+ "$ref": "#/$defs/text"
124
+ },
125
+ "port": {
126
+ "$ref": "#/$defs/port"
127
+ },
128
+ "transport": {
129
+ "enum": [
130
+ "tcp",
131
+ "udp"
132
+ ]
133
+ },
134
+ "purpose": {
135
+ "$ref": "#/$defs/text"
136
+ }
137
+ }
138
+ }
139
+ },
140
+ "remote_endpoints": {
141
+ "type": "array",
142
+ "maxItems": 256,
143
+ "items": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "required": [
147
+ "direction",
148
+ "host",
149
+ "port",
150
+ "transport",
151
+ "purpose"
152
+ ],
153
+ "properties": {
154
+ "direction": {
155
+ "enum": [
156
+ "inbound",
157
+ "outbound"
158
+ ]
159
+ },
160
+ "host": {
161
+ "$ref": "#/$defs/text"
162
+ },
163
+ "port": {
164
+ "$ref": "#/$defs/port"
165
+ },
166
+ "transport": {
167
+ "enum": [
168
+ "tcp",
169
+ "udp"
170
+ ]
171
+ },
172
+ "purpose": {
173
+ "$ref": "#/$defs/text"
174
+ }
175
+ }
176
+ }
177
+ },
178
+ "config_paths": {
179
+ "$ref": "#/$defs/pathArray"
180
+ },
181
+ "data_paths": {
182
+ "$ref": "#/$defs/pathArray"
183
+ },
184
+ "secret_refs": {
185
+ "$ref": "#/$defs/pathArray"
186
+ },
187
+ "depends_on": {
188
+ "type": "array",
189
+ "maxItems": 256,
190
+ "items": {
191
+ "$ref": "#/$defs/serviceId"
192
+ }
193
+ },
194
+ "commands": {
195
+ "type": "object",
196
+ "additionalProperties": false,
197
+ "description": "Display-only maintenance notes; Server Handbook never executes these strings.",
198
+ "properties": {
199
+ "status": {
200
+ "$ref": "#/$defs/text"
201
+ },
202
+ "start": {
203
+ "$ref": "#/$defs/text"
204
+ },
205
+ "stop": {
206
+ "$ref": "#/$defs/text"
207
+ },
208
+ "restart": {
209
+ "$ref": "#/$defs/text"
210
+ },
211
+ "logs": {
212
+ "$ref": "#/$defs/text"
213
+ }
214
+ }
215
+ },
216
+ "notes": {
217
+ "$ref": "#/$defs/textArray"
218
+ },
219
+ "evidence": {
220
+ "$ref": "#/$defs/pathArray"
221
+ },
222
+ "checks": {
223
+ "type": "array",
224
+ "maxItems": 16,
225
+ "items": {
226
+ "type": "object",
227
+ "additionalProperties": false,
228
+ "required": [
229
+ "type",
230
+ "url",
231
+ "expected_status",
232
+ "timeout_seconds"
233
+ ],
234
+ "properties": {
235
+ "type": {
236
+ "const": "http"
237
+ },
238
+ "url": {
239
+ "allOf": [
240
+ {
241
+ "$ref": "#/$defs/text"
242
+ }
243
+ ],
244
+ "maxLength": 2048,
245
+ "format": "uri",
246
+ "pattern": "^[hH][tT][tT][pP][sS]?://",
247
+ "not": {
248
+ "anyOf": [
249
+ {
250
+ "pattern": "\\s"
251
+ },
252
+ {
253
+ "pattern": "^[hH][tT][tT][pP][sS]?://[^/?#]*@"
254
+ }
255
+ ]
256
+ }
257
+ },
258
+ "expected_status": {
259
+ "type": "integer",
260
+ "minimum": 100,
261
+ "maximum": 599
262
+ },
263
+ "timeout_seconds": {
264
+ "type": "integer",
265
+ "minimum": 1,
266
+ "maximum": 30
267
+ }
268
+ }
269
+ }
270
+ }
271
+ },
272
+ "allOf": [
273
+ {
274
+ "if": {
275
+ "properties": {
276
+ "kind": {
277
+ "const": "compose"
278
+ },
279
+ "state": {
280
+ "const": "deployed"
281
+ }
282
+ },
283
+ "required": [
284
+ "kind",
285
+ "state"
286
+ ]
287
+ },
288
+ "then": {
289
+ "required": [
290
+ "compose"
291
+ ]
292
+ }
293
+ },
294
+ {
295
+ "if": {
296
+ "properties": {
297
+ "kind": {
298
+ "const": "systemd"
299
+ },
300
+ "state": {
301
+ "const": "deployed"
302
+ }
303
+ },
304
+ "required": [
305
+ "kind",
306
+ "state"
307
+ ]
308
+ },
309
+ "then": {
310
+ "required": [
311
+ "systemd_units"
312
+ ],
313
+ "properties": {
314
+ "systemd_units": {
315
+ "minItems": 1
316
+ }
317
+ }
318
+ }
319
+ }
320
+ ],
321
+ "$defs": {
322
+ "text": {
323
+ "type": "string",
324
+ "minLength": 1,
325
+ "maxLength": 4096,
326
+ "allOf": [
327
+ {
328
+ "pattern": "\\S"
329
+ },
330
+ {
331
+ "not": {
332
+ "pattern": "\u0000"
333
+ }
334
+ }
335
+ ]
336
+ },
337
+ "shortText": {
338
+ "allOf": [
339
+ {
340
+ "$ref": "#/$defs/text"
341
+ }
342
+ ],
343
+ "maxLength": 200
344
+ },
345
+ "serviceId": {
346
+ "type": "string",
347
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$",
348
+ "not": {
349
+ "pattern": "\\s"
350
+ }
351
+ },
352
+ "absolutePath": {
353
+ "allOf": [
354
+ {
355
+ "$ref": "#/$defs/text"
356
+ }
357
+ ],
358
+ "description": "POSIX, Windows drive, or UNC absolute syntax. Native-platform validation and resolved containment are enforced at runtime.",
359
+ "anyOf": [
360
+ {
361
+ "pattern": "^/"
362
+ },
363
+ {
364
+ "pattern": "^[A-Za-z]:[\\\\/]"
365
+ },
366
+ {
367
+ "pattern": "^\\\\"
368
+ }
369
+ ]
370
+ },
371
+ "textArray": {
372
+ "type": "array",
373
+ "maxItems": 256,
374
+ "items": {
375
+ "$ref": "#/$defs/text"
376
+ }
377
+ },
378
+ "pathArray": {
379
+ "type": "array",
380
+ "maxItems": 256,
381
+ "items": {
382
+ "$ref": "#/$defs/absolutePath"
383
+ }
384
+ },
385
+ "port": {
386
+ "type": "integer",
387
+ "minimum": 1,
388
+ "maximum": 65535
389
+ }
390
+ }
391
+ }
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: server-handbook
3
+ description: Adopt, inspect, deploy, or hand off projects on a server using Server Handbook's registry and managed operation receipts. Use for service maintenance and server context recovery, not unrelated coding tasks.
4
+ ---
5
+
6
+ # Server Handbook
7
+
8
+ Use the installed CLI or `handbook_*` MCP tools to find the actual service and its maintenance instructions. The user may have chosen custom paths; do not assume `/srv/workspace` or move existing projects.
9
+
10
+ Start with `handoff` / `handbook_handoff`, then inspect the target service and read its registered README. Read existing workspace and project AGENTS instructions. Preserve explicit user choices and authorization; this skill supplies a workflow, not additional permission.
11
+
12
+ Treat these as different evidence:
13
+
14
+ - Declaration: desired metadata, paths and current registry revision.
15
+ - Observation: a fresh query, possibly `unknown` if permissions or tools are unavailable.
16
+ - Receipt: what a specific managed operation actually executed and verified at a recorded time.
17
+
18
+ For deployment, inspect the plan and source drift first. Apply only the requested registered service action, with its current expected revision and a meaningful purpose. Inspect the resulting receipt. Process readiness alone does not establish business correctness; report the configured check scope and any missing checks.
19
+
20
+ For metadata changes, edit the service's declared source file, then register it with the current expected revision. Registration and managed execution update their own README block automatically; retain manual notes. Run doctor and inspect reported drift or unfinished operations before reporting completion.
21
+
22
+ If work was interrupted, inspect its receipt and live state. Recovery only marks abandoned work interrupted; it does not replay commands or roll back external effects. Do not rerun first-time database initialization as routine maintenance.
23
+
24
+ Read [the workflow reference](references/workflow.md) when preparing a new project, adopting an old one, or handling an incomplete operation. Direct shell actions outside this tool are not fully recorded; explain such changes rather than claiming a complete audit trail.
@@ -0,0 +1,39 @@
1
+ # Using the shared server workflow
2
+
3
+ Commands below assume the workspace config is discoverable. Otherwise add `--config /absolute/path/to/.server-handbook/config.json` before the subcommand. Use `--help` to confirm installed options.
4
+
5
+ ## Locate and inspect
6
+
7
+ ```bash
8
+ server-handbook handoff
9
+ server-handbook list
10
+ server-handbook show SERVICE_ID
11
+ server-handbook doctor
12
+ ```
13
+
14
+ Follow returned README paths and the recorded Compose files or systemd units. `overview --runtime` adds current host observations. Missing permissions return unknown, not stopped. Descriptor `commands` values are reference text and are never an execution mechanism.
15
+
16
+ ## Prepare or adopt
17
+
18
+ `project new` and `adopt` preview by default. `--apply` writes their reviewed preparation files; neither deploys or registers a service. Adoption retains the existing home and manual documentation. Review discovered candidates before deciding they are current services.
19
+
20
+ Fill `home/deploy/registry/SERVICE_ID.json` with actual metadata. Keep credentials in their existing controlled configuration, and record only paths in `secret_refs`. State `prepared` remains honest until actual deployment is established. Optional profiles guide development without replacing repository-specific conventions.
21
+
22
+ ## Register and deploy
23
+
24
+ ```bash
25
+ server-handbook register /absolute/service-home/deploy/registry/SERVICE_ID.json --note 'Actual metadata change and known verification scope'
26
+ server-handbook plan SERVICE_ID --action up
27
+ server-handbook apply SERVICE_ID --action up --expect-revision REVISION --note 'Purpose of this authorized deployment'
28
+ server-handbook operations --service SERVICE_ID
29
+ server-handbook operation OPERATION_ID
30
+ server-handbook doctor
31
+ ```
32
+
33
+ On an existing declaration, registration also requires `--expect-revision REVISION`. Resolve conflicts by rereading and comparing; do not overwrite blindly. Compose `up/restart/stop` applies only to the named service; dependencies must already be running. systemd `start/restart/stop/reload` applies to the registered units.
34
+
35
+ Read the receipt even if the process started. It distinguishes execution failure, check failure, process-only verification, configured HTTP verification, and documentation refresh failure. For business acceptance beyond those probes, use the project's existing tests under the user's task and record the additional outcome honestly in maintenance notes.
36
+
37
+ ## Incomplete work
38
+
39
+ An unfinished operation blocks another managed action on the same service. Inspect both the receipt and current state. `recover OPERATION_ID --note ...` refuses a live owner and marks abandoned work interrupted. It never performs rollback, retries commands or resets data. Build a new plan only after understanding remaining effects.
server_handbook/cli.py ADDED
@@ -0,0 +1,157 @@
1
+ """One CLI over the shared validation, registration and execution core."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+
8
+ from . import __version__
9
+ from .errors import HandbookError
10
+
11
+
12
+ def parser():
13
+ root = argparse.ArgumentParser(description="Server Handbook: server context, deployment registry, and tracked operations.")
14
+ root.add_argument("--version", action="version", version=f"Server Handbook {__version__}")
15
+ root.add_argument("--config", help="Workspace config; also SERVER_HANDBOOK_CONFIG or nearest .server-handbook/config.json")
16
+ commands = root.add_subparsers(dest="command", required=True)
17
+ init = commands.add_parser("init", help="Preview initialization; --apply creates only tool-owned files and managed blocks")
18
+ init.add_argument("--workspace", required=True)
19
+ init.add_argument("--name", default="Server Handbook")
20
+ init.add_argument("--project-root", action="append")
21
+ init.add_argument("--service-root", action="append")
22
+ init.add_argument("--records-dir")
23
+ init.add_argument("--apply", action="store_true")
24
+ project = commands.add_parser("project", help="Prepare a new project without claiming a deployment")
25
+ new = project.add_subparsers(dest="project_command", required=True).add_parser("new")
26
+ new.add_argument("id")
27
+ new.add_argument("--name", required=True)
28
+ new.add_argument("--kind", choices=("project", "compose", "systemd"), default="project")
29
+ new.add_argument("--home")
30
+ new.add_argument("--profile", choices=("generic", "python", "node", "java"), default="generic")
31
+ new.add_argument("--apply", action="store_true")
32
+ adopt = commands.add_parser("adopt", help="Prepare an existing project; retain its files and manual notes")
33
+ for name in ("home", "id", "name"):
34
+ adopt.add_argument(f"--{name}", required=True)
35
+ adopt.add_argument("--kind", choices=("project", "compose", "systemd", "desktop", "toolchain"), default="project")
36
+ adopt.add_argument("--compose-file", action="append")
37
+ adopt.add_argument("--compose-project")
38
+ adopt.add_argument("--compose-service")
39
+ adopt.add_argument("--unit", action="append")
40
+ adopt.add_argument("--apply", action="store_true")
41
+ register = commands.add_parser("register", help="Register a reviewed descriptor and refresh its managed README block")
42
+ register.add_argument("file")
43
+ register.add_argument("--note", required=True)
44
+ register.add_argument("--expect-revision", type=int)
45
+ listed = commands.add_parser("list", help="List registered metadata, not live health")
46
+ listed.add_argument("--offset", type=int, default=0)
47
+ listed.add_argument("--limit", type=int, default=20)
48
+ for name in ("show", "history"):
49
+ commands.add_parser(name).add_argument("id")
50
+ commands.add_parser("overview").add_argument("--runtime", action="store_true")
51
+ commands.add_parser("discover", help="Find candidates read-only; never auto-register")
52
+ commands.add_parser("doctor", help="Check source drift, registry integrity and unfinished operations").add_argument("--runtime", action="store_true")
53
+ docs = commands.add_parser("docs")
54
+ render = docs.add_subparsers(dest="docs_command", required=True).add_parser("render")
55
+ render.add_argument("id")
56
+ render.add_argument("--apply", action="store_true")
57
+ commands.add_parser("handoff", help="Compact zero-context entry with next steps")
58
+ skill = commands.add_parser("skill", help="Export the bundled portable Agent Skill without changing client settings")
59
+ export = skill.add_subparsers(dest="skill_command", required=True).add_parser("export")
60
+ export.add_argument("destination")
61
+ export.add_argument("--apply", action="store_true")
62
+ for name in ("plan", "apply"):
63
+ operation = commands.add_parser(name, help="Plan a registered operation" if name == "plan" else "Execute and record a registered operation")
64
+ operation.add_argument("id")
65
+ operation.add_argument("--action", required=True, choices=("up", "start", "restart", "stop", "reload"))
66
+ if name == "apply":
67
+ operation.add_argument("--expect-revision", required=True, type=int)
68
+ operation.add_argument("--note", required=True)
69
+ operations = commands.add_parser("operations")
70
+ operations.add_argument("--service")
71
+ operations.add_argument("--limit", type=int, default=20)
72
+ commands.add_parser("operation").add_argument("id")
73
+ recovery = commands.add_parser("recover", help="Mark an abandoned operation interrupted; never replay or roll back commands")
74
+ recovery.add_argument("id")
75
+ recovery.add_argument("--note", required=True)
76
+ commands.add_parser("mcp", help="Serve stdio MCP; readonly unless explicitly enabled").add_argument("--allow-write", action="store_true")
77
+ return root
78
+
79
+
80
+ def dispatch(args):
81
+ from .config import initialize, load_config
82
+ if args.command == "init":
83
+ return initialize(args.workspace, name=args.name, project_roots=args.project_root,
84
+ service_roots=args.service_root, records_dir=args.records_dir, apply=args.apply)
85
+ if args.command == "skill":
86
+ from .documents import export_skill
87
+ return export_skill(args.destination, apply=args.apply)
88
+ config = load_config(args.config)
89
+ from . import documents, operations, registry, runtime
90
+ if args.command == "project":
91
+ return documents.project_new(config, args.id, args.name, kind=args.kind, home=args.home,
92
+ profile=args.profile, apply=args.apply)
93
+ if args.command == "adopt":
94
+ return documents.adopt(config, args.home, args.id, args.name, kind=args.kind,
95
+ compose_files=args.compose_file, compose_project=args.compose_project,
96
+ compose_service=args.compose_service, systemd_units=args.unit, apply=args.apply)
97
+ if args.command == "register":
98
+ return documents.register_and_render(config, args.file, args.note, expect_revision=args.expect_revision)
99
+ if args.command == "list":
100
+ return registry.list_services(config, offset=args.offset, limit=args.limit)
101
+ if args.command == "show":
102
+ return registry.get_service(config, args.id)
103
+ if args.command == "history":
104
+ return {"history": registry.service_history(config, args.id)}
105
+ if args.command == "overview":
106
+ return runtime.overview(config, include_runtime=args.runtime)
107
+ if args.command == "discover":
108
+ return runtime.discover(config)
109
+ if args.command == "doctor":
110
+ return runtime.doctor(config, include_runtime=args.runtime)
111
+ if args.command == "docs":
112
+ return documents.render_service_docs(config, args.id, apply=args.apply)
113
+ if args.command == "handoff":
114
+ return documents.handoff(config)
115
+ if args.command == "plan":
116
+ return operations.plan_operation(config, args.id, args.action)
117
+ if args.command == "apply":
118
+ return operations.apply_operation(config, args.id, args.action, args.expect_revision, args.note)
119
+ if args.command == "operations":
120
+ return {"operations": operations.list_operations(config, service_id=args.service, limit=args.limit)}
121
+ if args.command == "operation":
122
+ return operations.get_operation(config, args.id)
123
+ if args.command == "recover":
124
+ return operations.recover_operation(config, args.id, args.note)
125
+ raise HandbookError("Unknown command; use --help.")
126
+
127
+
128
+ def main(argv=None):
129
+ if hasattr(sys.stdout, "reconfigure"):
130
+ sys.stdout.reconfigure(encoding="utf-8")
131
+ sys.stderr.reconfigure(encoding="utf-8")
132
+ args = parser().parse_args(argv)
133
+ try:
134
+ if args.command == "mcp":
135
+ from .mcp import main as mcp_main
136
+ values = (["--config", args.config] if args.config else []) + (["--allow-write"] if args.allow_write else [])
137
+ return mcp_main(values)
138
+ result = dispatch(args)
139
+ if result is not None:
140
+ print(json.dumps(result, ensure_ascii=False, indent=2))
141
+ if args.command == "apply" and result["status"] != "succeeded":
142
+ return 1
143
+ if args.command == "doctor" and (not result["valid"] or result["unfinished_operations"]):
144
+ return 1
145
+ return 0
146
+ except ImportError as error:
147
+ if args.command == "mcp":
148
+ print("MCP extra is unavailable. Install 'server-handbook[mcp]' in this environment.", file=sys.stderr)
149
+ return 1
150
+ raise error
151
+ except (HandbookError, OSError) as error:
152
+ print(f"Error: {error}", file=sys.stderr)
153
+ return 1
154
+
155
+
156
+ if __name__ == "__main__":
157
+ raise SystemExit(main())