@reunionstudio/airlock-mcp 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 (57) hide show
  1. package/.agents/skills/airlock-mcp/SKILL.md +122 -0
  2. package/.agents/skills/airlock-mcp/agents/openai.yaml +4 -0
  3. package/LICENSE +187 -0
  4. package/README.md +126 -0
  5. package/SECURITY.md +31 -0
  6. package/bin/airlock-mcp.mjs +5 -0
  7. package/docs/architecture.md +82 -0
  8. package/docs/install-surface.md +112 -0
  9. package/docs/ooda-loop.md +40 -0
  10. package/docs/spec-workbench-architecture.md +161 -0
  11. package/docs/spec-workspace.md +33 -0
  12. package/docs/workflows.md +229 -0
  13. package/package.json +46 -0
  14. package/patterns/blank/README.md +14 -0
  15. package/patterns/blank/sample.records.json +19 -0
  16. package/patterns/blank/spec.config.json +72 -0
  17. package/patterns/guest-access/individual-isolation.md +27 -0
  18. package/patterns/guest-access/role-isolation.md +26 -0
  19. package/patterns/guest-access/shared-contribution.md +25 -0
  20. package/patterns/manifest.json +16 -0
  21. package/patterns/spec-types/commitment.md +24 -0
  22. package/patterns/spec-types/observation.md +22 -0
  23. package/patterns/spec-types/reconciliation.md +19 -0
  24. package/patterns/spec-types/reference-master-data.md +21 -0
  25. package/patterns/starter-posts/README.md +32 -0
  26. package/patterns/starter-posts/sample.records.json +27 -0
  27. package/patterns/starter-posts/spec.config.json +135 -0
  28. package/schemas/airlock-mcp-workspace.schema.json +14 -0
  29. package/setup.py +41 -0
  30. package/src/airlock_mcp/__init__.py +3 -0
  31. package/src/airlock_mcp/__main__.py +5 -0
  32. package/src/airlock_mcp/art.py +26 -0
  33. package/src/airlock_mcp/bootstrap.py +161 -0
  34. package/src/airlock_mcp/cli.py +450 -0
  35. package/src/airlock_mcp/jsonio.py +56 -0
  36. package/src/airlock_mcp/manage.py +247 -0
  37. package/src/airlock_mcp/models.py +43 -0
  38. package/src/airlock_mcp/patterns.py +49 -0
  39. package/src/airlock_mcp/project.py +39 -0
  40. package/src/airlock_mcp/records.py +43 -0
  41. package/src/airlock_mcp/specs.py +110 -0
  42. package/src/airlock_mcp/sql.py +15 -0
  43. package/src/airlock_mcp/summary.py +115 -0
  44. package/src/airlock_mcp/updater.py +76 -0
  45. package/src/airlock_mcp/validation.py +334 -0
  46. package/src/airlock_mcp/workspace.py +223 -0
  47. package/src/cli.mjs +89 -0
  48. package/src/install.mjs +100 -0
  49. package/src/mcp.mjs +184 -0
  50. package/src/text.mjs +108 -0
  51. package/src/workbench.mjs +368 -0
  52. package/workspaces/_template/brief.md +18 -0
  53. package/workspaces/_template/decisions.md +40 -0
  54. package/workspaces/_template/questions.md +9 -0
  55. package/workspaces/_template/review.md +21 -0
  56. package/workspaces/_template/sample.records.json +19 -0
  57. package/workspaces/_template/spec.config.json +72 -0
@@ -0,0 +1,25 @@
1
+ # Guest Access: Shared Contribution
2
+
3
+ Use this when many people or agents contribute to the same governed dataset.
4
+
5
+ Good fit:
6
+
7
+ - posts and feedback
8
+ - general ops issues
9
+ - shared observations
10
+ - public append-only signal streams
11
+
12
+ Design notes:
13
+
14
+ - Enable a public append path for normal contribution.
15
+ - Consider a public read path only when contributors may safely see the shared
16
+ data.
17
+ - Use workflow for moderation, triage, and pushback.
18
+ - Use typed routing fields or tags for simple triage; create specialized specs
19
+ only when lifecycle, evidence, validation, or ownership diverges.
20
+
21
+ Questions to answer:
22
+
23
+ - Can contributors read all records, or only append?
24
+ - Does moderation happen before records become visible?
25
+ - What downstream agent or human watches the shared dataset?
@@ -0,0 +1,16 @@
1
+ {
2
+ "patterns": [
3
+ {
4
+ "name": "blank",
5
+ "title": "Blank draft",
6
+ "summary": "Minimal observation-style draft for a known process.",
7
+ "directory": "blank"
8
+ },
9
+ {
10
+ "name": "posts",
11
+ "title": "Posts feedback loop",
12
+ "summary": "Small governed feedback stream for discovering what to build next.",
13
+ "directory": "starter-posts"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,24 @@
1
+ # Spec Type: Commitment
2
+
3
+ A commitment spec records a decision or promise the business intends to honor or
4
+ send downstream.
5
+
6
+ Good fit:
7
+
8
+ - approved budget requests
9
+ - payment commitments
10
+ - outbound product updates
11
+ - accepted offers or invitations
12
+
13
+ Design notes:
14
+
15
+ - Distinguish the observed inputs from the committed output.
16
+ - Use workflow for approval and pushback.
17
+ - Use references to link back to source observations, requests, projects, or
18
+ counterparties.
19
+ - Use expectations when commitments must happen in order or by a due date.
20
+
21
+ Avoid:
22
+
23
+ - copying every upstream observation field into the commitment
24
+ - hiding approval state in payload columns instead of Airlock workflow
@@ -0,0 +1,22 @@
1
+ # Spec Type: Observation
2
+
3
+ An observation spec records something a person, agent, export, browser capture,
4
+ or API observed.
5
+
6
+ Use typed columns for:
7
+
8
+ - observed object id
9
+ - source system or URL
10
+ - observed or captured timestamp
11
+ - status, amount, category, or score used in decisions
12
+ - capture method
13
+
14
+ Use attachments for screenshots, PDFs, exports, receipts, or other evidence.
15
+ Use a validated `variant` for optional source payload context.
16
+
17
+ Avoid:
18
+
19
+ - using Airlock load time as the event timestamp
20
+ - placing raw attachment bytes in payload fields
21
+ - mixing observations and commitments in one row unless `record_type` is an
22
+ intentional design choice
@@ -0,0 +1,19 @@
1
+ # Spec Type: Reconciliation
2
+
3
+ A reconciliation spec compares two or more systems, datasets, or expectations
4
+ and records the business difference that needs attention.
5
+
6
+ Good fit:
7
+
8
+ - commerce listing gaps
9
+ - payment versus invoice matching
10
+ - ad spend versus campaign records
11
+ - inventory or order discrepancies
12
+
13
+ Design notes:
14
+
15
+ - Make the row grain the discrepancy, match group, or reconciliation finding.
16
+ - Preserve stable keys from every compared system.
17
+ - Use typed fields for match status, amounts, dates, and source identifiers.
18
+ - Use a validated `variant` for optional comparison detail.
19
+ - Use workflow when a human must resolve or accept the finding.
@@ -0,0 +1,21 @@
1
+ # Spec Type: Reference Or Master Data
2
+
3
+ A reference/master-data spec provides controlled data that other specs validate
4
+ against or read from.
5
+
6
+ Good fit:
7
+
8
+ - projects
9
+ - departments
10
+ - vendors
11
+ - employees
12
+ - locations
13
+ - chart-of-accounts segments
14
+
15
+ Design notes:
16
+
17
+ - Use stable keys and readable aliases.
18
+ - Keep lifecycle simple unless changes require review.
19
+ - Use references from transactional specs into these keys.
20
+ - Consider a read-only reference spec over a materialized table for high-read
21
+ agent workflows.
@@ -0,0 +1,32 @@
1
+ # Starter Pattern: Posts
2
+
3
+ Use `posts` when the team does not yet know which larger Airlock spec to build.
4
+ It creates a small governed feedback loop:
5
+
6
+ - a person or agent submits a post, request, observation, or response
7
+ - optional structured details are captured in a validated `variant`
8
+ - other people or agents can review the feed and decide what spec to build next
9
+
10
+ This pattern is intentionally humble. It gives the organization a safe place to
11
+ observe demand before designing a bigger workflow.
12
+
13
+ ## When To Use
14
+
15
+ - You are starting from fuzzy process needs.
16
+ - You want feedback from users inside Snowflake.
17
+ - You want agents to ask for, respond to, or triage requests.
18
+ - You need a governed signal stream before committing to specialized specs.
19
+
20
+ ## Adapt Carefully
21
+
22
+ Keep the core small:
23
+
24
+ - one row is one post
25
+ - `post_id` is stable and retry-safe
26
+ - `body` is the human-readable signal
27
+ - `tags` supports light routing
28
+ - `details` holds optional structured context
29
+
30
+ Create separate specs later when posts become reimbursements, budget requests,
31
+ project issues, support tickets, or outbound commitments with different
32
+ validation, evidence, workflow, or ownership.
@@ -0,0 +1,27 @@
1
+ {
2
+ "spec_name": "posts",
3
+ "filename": "post_001",
4
+ "records": [
5
+ {
6
+ "post_id": "POST-001",
7
+ "reply_to_post_id": "",
8
+ "submitted_by": "agent.deb",
9
+ "posted_at": "2026-06-13 09:00:00",
10
+ "body": "Please make reimbursements easier to prepare and submit.",
11
+ "tags": "#request #finance #reimbursements",
12
+ "related_area": "finance",
13
+ "related_process": "employee reimbursements",
14
+ "details": {
15
+ "source": {
16
+ "system": "codex"
17
+ },
18
+ "request": {
19
+ "desired_outcome": "An approved agent can prepare a draft reimbursement with receipt evidence."
20
+ },
21
+ "agent": {
22
+ "name": "Deb"
23
+ }
24
+ }
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,135 @@
1
+ {
2
+ "core_config": {
3
+ "spec_name": "posts",
4
+ "spec_alias": "Posts",
5
+ "description": "Governed feedback, requests, observations, and responses from people or agents.",
6
+ "owner_role": "app_admin",
7
+ "is_published": false,
8
+ "is_archived": false
9
+ },
10
+ "column_config": [
11
+ {
12
+ "name": "post_id",
13
+ "type": "string",
14
+ "description": "Stable post identifier supplied by the submitting person or agent.",
15
+ "tests": ["not_null", "unique"]
16
+ },
17
+ {
18
+ "name": "reply_to_post_id",
19
+ "type": "string",
20
+ "description": "Optional parent post id when this post is a response.",
21
+ "tests": []
22
+ },
23
+ {
24
+ "name": "submitted_by",
25
+ "type": "string",
26
+ "description": "Business-facing submitter label or upstream user id.",
27
+ "tests": ["not_null"]
28
+ },
29
+ {
30
+ "name": "posted_at",
31
+ "type": "datetime",
32
+ "description": "Business timestamp when the post was authored or captured.",
33
+ "format": "%Y-%m-%d %H:%M:%S",
34
+ "tests": ["not_null"]
35
+ },
36
+ {
37
+ "name": "body",
38
+ "type": "string",
39
+ "description": "Plain-language feedback, request, observation, or response.",
40
+ "tests": ["not_null"]
41
+ },
42
+ {
43
+ "name": "tags",
44
+ "type": "string",
45
+ "description": "Optional routing tags such as #request, #bug, #finance, or #spec-gap.",
46
+ "tests": []
47
+ },
48
+ {
49
+ "name": "related_area",
50
+ "type": "string",
51
+ "description": "Optional business area such as finance, operations, support, product, or commerce.",
52
+ "tests": []
53
+ },
54
+ {
55
+ "name": "related_process",
56
+ "type": "string",
57
+ "description": "Optional process this post may lead to, such as reimbursements or budget requests.",
58
+ "tests": []
59
+ },
60
+ {
61
+ "name": "details",
62
+ "type": "variant",
63
+ "description": "Optional structured context for source, request, evidence metadata, or agent notes.",
64
+ "tests": []
65
+ }
66
+ ],
67
+ "rules": [
68
+ {
69
+ "type": "variant_shape",
70
+ "field": "details",
71
+ "allowed_root_keys": ["source", "request", "evidence", "agent"],
72
+ "paths": [
73
+ {
74
+ "json_path": "$.source.system",
75
+ "type": "string",
76
+ "required": false
77
+ },
78
+ {
79
+ "json_path": "$.request.desired_outcome",
80
+ "type": "string",
81
+ "required": false
82
+ },
83
+ {
84
+ "json_path": "$.evidence.url",
85
+ "type": "string",
86
+ "required": false
87
+ },
88
+ {
89
+ "json_path": "$.agent.name",
90
+ "type": "string",
91
+ "required": false
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "file_rules": {
97
+ "file_format": {
98
+ "file_type": "csv",
99
+ "record_delimiter": "\n",
100
+ "field_delimiter": ",",
101
+ "field_optionally_enclosed_by": "\"",
102
+ "escape_unenclosed_field": "\\",
103
+ "encoding": "UTF8",
104
+ "parse_header": true,
105
+ "save_header": true
106
+ }
107
+ },
108
+ "attachment_policy": {
109
+ "attachments_enabled": true,
110
+ "attachment_required": false
111
+ },
112
+ "guest_access": {
113
+ "isolated_directories_enabled": false,
114
+ "public_folder": {
115
+ "enabled": true,
116
+ "subfolders": {
117
+ "append_access": {
118
+ "enabled": true
119
+ },
120
+ "read_access": {
121
+ "enabled": true
122
+ },
123
+ "full_access": {
124
+ "enabled": false
125
+ }
126
+ }
127
+ },
128
+ "guest_roles": [
129
+ {
130
+ "role_name": "agent",
131
+ "access_level": "append_access"
132
+ }
133
+ ]
134
+ }
135
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Airlock MCP Workspace",
4
+ "type": "object",
5
+ "required": ["brief", "decisions", "spec_config", "sample_records"],
6
+ "properties": {
7
+ "brief": { "type": "string" },
8
+ "decisions": { "type": "string" },
9
+ "questions": { "type": "string" },
10
+ "review": { "type": "string" },
11
+ "spec_config": { "type": "object" },
12
+ "sample_records": { "type": "object" }
13
+ }
14
+ }
package/setup.py ADDED
@@ -0,0 +1,41 @@
1
+ from glob import glob
2
+ from os.path import isfile
3
+
4
+ from setuptools import find_packages, setup
5
+
6
+
7
+ def data_files():
8
+ files = [
9
+ (
10
+ "airlock_mcp/.agents/skills/airlock-mcp",
11
+ [path for path in glob(".agents/skills/airlock-mcp/*") if isfile(path)],
12
+ ),
13
+ (
14
+ "airlock_mcp/.agents/skills/airlock-mcp/agents",
15
+ [path for path in glob(".agents/skills/airlock-mcp/agents/*") if isfile(path)],
16
+ ),
17
+ ("airlock_mcp/patterns", ["patterns/manifest.json"]),
18
+ ("airlock_mcp/workspaces/_template", glob("workspaces/_template/*")),
19
+ ]
20
+ for pattern_dir in glob("patterns/*"):
21
+ if pattern_dir.endswith("manifest.json"):
22
+ continue
23
+ if glob(f"{pattern_dir}/*"):
24
+ files.append((f"airlock_mcp/{pattern_dir}", glob(f"{pattern_dir}/*")))
25
+ return files
26
+
27
+
28
+ setup(
29
+ name="airlock-mcp",
30
+ version="0.1.0",
31
+ description="Codex-first workbench and CLI for drafting Airlock specs.",
32
+ packages=find_packages("src"),
33
+ package_dir={"": "src"},
34
+ python_requires=">=3.9",
35
+ data_files=data_files(),
36
+ entry_points={
37
+ "console_scripts": [
38
+ "airlock-mcp=airlock_mcp.cli:main",
39
+ ],
40
+ },
41
+ )
@@ -0,0 +1,3 @@
1
+ """Airlock MCP CLI package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from . import __version__
4
+
5
+
6
+ MCP_MARK = r"""
7
+ airlock-mcp
8
+
9
+ __
10
+ ____/ /__ draft specs
11
+ / __ / _ \ check the shape
12
+ /_/ /_/\___/ forge the handoff
13
+
14
+ _________
15
+ ____/_______/ \____
16
+ \_______\_/
17
+ """
18
+
19
+
20
+ def about_text() -> str:
21
+ return f"""{MCP_MARK.rstrip()}
22
+
23
+ version: {__version__}
24
+ commands: init-repo, init, import-spec, clone, rename, archive, restore, list-workspaces, summary, next, check, export-csv, render-sql, self-update
25
+ principle: Codex is the conversation; files are memory; Airlock is authority.
26
+ """
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from .project import repo_root
8
+
9
+
10
+ AGENTS_MD = """# Airlock Spec Workspace Guidance
11
+
12
+ This repo is an Airlock MCP workspace for drafting Airlock specs with Codex.
13
+
14
+ ## Product Shape
15
+
16
+ The public install surface is Airlock MCP, for example
17
+ `npx @reunionstudio/airlock-mcp install`. Airlock MCP is the single installed
18
+ interface for building specs, using specs, pulling and pushing governed data,
19
+ and discovering improvements from real use cases. This project repo remains
20
+ the durable memory for the specs being drafted.
21
+
22
+ ## Repo Naming
23
+
24
+ When helping create a new specs repo, ask for the project or organization name
25
+ and suggest `<slug>-specs`: lowercase the name, replace spaces with hyphens,
26
+ and append `-specs`. For example, `Home` becomes `home-specs`.
27
+
28
+ Reserve `airlock-specs` for the canonical reusable Airlock spec library. A
29
+ team, customer, domain, or project should use its own scoped repo name such as
30
+ `home-specs`, `acme-finance-specs`, or `customer-onboarding-specs`.
31
+
32
+ Airlock itself does not create this repo. Codex can create it locally while
33
+ helping the user start an Airlock MCP project, then Airlock receives the
34
+ finished spec later. Start in Codex, not Snowflake Cortex. Snow CLI or Cortex
35
+ only matters later when validating, creating, or operating specs against an
36
+ installed Airlock app.
37
+
38
+ ## Starting A Spec Project
39
+
40
+ After bootstrap, welcome the user and orient before creating a workspace. Ask
41
+ whether they want to:
42
+
43
+ - brainstorm possible specs with the OODA loop
44
+ - start from a known process they already have in mind
45
+ - create a shared `posts` feedback loop for humans and agents to submit
46
+ requests, observations, and responses
47
+
48
+ Create `posts` only when the user chooses a feedback loop or explicitly asks
49
+ for the posts pattern.
50
+
51
+ ## Working Style
52
+
53
+ - Use the repo-scoped `$airlock-mcp` skill for spec drafting, review, and
54
+ pattern selection.
55
+ - Keep drafts small and concrete. Prefer one useful governed output over a
56
+ large speculative process map.
57
+ - Preserve decisions in workspace files so future Codex sessions can resume
58
+ without relying on chat memory.
59
+ - Use `airlock-mcp list-workspaces` before guessing which draft to resume.
60
+ - Use `airlock-mcp summary <workspace>` and `airlock-mcp next <workspace>`
61
+ before editing an existing draft.
62
+ - Keep `sample.records.json` as the editable authoring shape, then use
63
+ `airlock-mcp export-csv <workspace>` when Airlock-ready CSV examples are
64
+ needed.
65
+ - Treat Airlock as the execution authority. Local checks are guardrails, not a
66
+ replacement for installed Airlock validation.
67
+
68
+ ## Spec Design Priorities
69
+
70
+ Resolve these before final JSON:
71
+
72
+ - row grain
73
+ - durable identifiers
74
+ - business event timestamps
75
+ - typed columns versus validated variants
76
+ - attachment evidence
77
+ - guest access and path isolation
78
+ - workflow and pushback
79
+ - references and expectations
80
+ - delegation and agent identity
81
+ - observe-orient-decide-act loop
82
+
83
+ Do not encode Airlock lifecycle state, reviewer notes, approval status, or
84
+ workflow transitions as submitted payload fields unless they are true upstream
85
+ business facts.
86
+ """
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class BootstrapResult:
91
+ root: Path
92
+ created: tuple[Path, ...]
93
+ kept: tuple[Path, ...]
94
+
95
+
96
+ def _write_text_if_needed(path: Path, text: str, *, force: bool) -> str:
97
+ if path.exists() and not force:
98
+ return "kept"
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ path.write_text(text, encoding="utf-8")
101
+ return "created"
102
+
103
+
104
+ def _copy_file_if_needed(source: Path, target: Path, *, force: bool) -> str:
105
+ if target.exists() and not force:
106
+ return "kept"
107
+ target.parent.mkdir(parents=True, exist_ok=True)
108
+ shutil.copy2(source, target)
109
+ return "created"
110
+
111
+
112
+ def bootstrap_repo(target: Path, *, force: bool = False) -> BootstrapResult:
113
+ root = target.expanduser().resolve()
114
+ if root.exists() and not root.is_dir():
115
+ raise NotADirectoryError(str(root))
116
+
117
+ created: list[Path] = []
118
+ kept: list[Path] = []
119
+ root.mkdir(parents=True, exist_ok=True)
120
+
121
+ workspace_root = root / "workspaces"
122
+ if workspace_root.exists():
123
+ kept.append(workspace_root)
124
+ else:
125
+ workspace_root.mkdir()
126
+ created.append(workspace_root)
127
+
128
+ agents_path = root / "AGENTS.md"
129
+ bucket = created if _write_text_if_needed(agents_path, AGENTS_MD, force=force) == "created" else kept
130
+ bucket.append(agents_path)
131
+
132
+ source_skill = repo_root() / ".agents" / "skills" / "airlock-mcp"
133
+ if not source_skill.exists():
134
+ raise FileNotFoundError(str(source_skill))
135
+
136
+ target_skill = root / ".agents" / "skills" / "airlock-mcp"
137
+ for source_file in sorted(path for path in source_skill.rglob("*") if path.is_file()):
138
+ relative = source_file.relative_to(source_skill)
139
+ target_file = target_skill / relative
140
+ bucket = created if _copy_file_if_needed(source_file, target_file, force=force) == "created" else kept
141
+ bucket.append(target_file)
142
+
143
+ return BootstrapResult(root=root, created=tuple(created), kept=tuple(kept))
144
+
145
+
146
+ def format_bootstrap_result(result: BootstrapResult) -> str:
147
+ lines = [f"initialized {result.root}"]
148
+ for path in result.created:
149
+ lines.append(f"created {path.relative_to(result.root)}")
150
+ for path in result.kept:
151
+ lines.append(f"kept {path.relative_to(result.root)}")
152
+ lines.extend(
153
+ [
154
+ "",
155
+ "next:",
156
+ "1. Open this repo in Codex.",
157
+ "2. Ask: Use Airlock to help me build and use specs.",
158
+ "3. Choose OODA brainstorming, a known process, or a posts feedback loop.",
159
+ ]
160
+ )
161
+ return "\n".join(lines)