tylor-mcp 1.0.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 (101) hide show
  1. package/.aws-setup.sh +25 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.mcp.json +12 -0
  4. package/AGENTS.md +93 -0
  5. package/CLAUDE.md +99 -0
  6. package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
  7. package/LICENSE +21 -0
  8. package/README.md +146 -0
  9. package/assets/tylor_logo.png +0 -0
  10. package/assets/tylor_threads_concept.png +0 -0
  11. package/bin/tylor.js +23 -0
  12. package/hooks/kill-thread-trigger.sh +7 -0
  13. package/hooks/post-tool-use-code-index.sh +7 -0
  14. package/hooks/session-checkpoint.sh +7 -0
  15. package/hooks/session-start.sh +7 -0
  16. package/install.py +401 -0
  17. package/install.sh +260 -0
  18. package/package.json +24 -0
  19. package/pytest.ini +2 -0
  20. package/registry.json +26 -0
  21. package/server/.env.example +24 -0
  22. package/server/__init__.py +0 -0
  23. package/server/config.py +89 -0
  24. package/server/main.py +93 -0
  25. package/server/personas/analyst.md +15 -0
  26. package/server/personas/ceo.md +14 -0
  27. package/server/personas/code_agent.md +15 -0
  28. package/server/personas/cto.md +14 -0
  29. package/server/provision.py +260 -0
  30. package/server/provision_opensearch.py +154 -0
  31. package/server/requirements.txt +26 -0
  32. package/server/storage/__init__.py +0 -0
  33. package/server/storage/dynamo.py +399 -0
  34. package/server/storage/json_store.py +359 -0
  35. package/server/storage/opensearch.py +194 -0
  36. package/server/storage/s3.py +96 -0
  37. package/server/storage/tests/__init__.py +0 -0
  38. package/server/storage/tests/test_dynamo.py +452 -0
  39. package/server/storage/tests/test_json_store.py +226 -0
  40. package/server/storage/tests/test_opensearch.py +270 -0
  41. package/server/storage/tests/test_s3.py +125 -0
  42. package/server/tests/__init__.py +0 -0
  43. package/server/tests/test_install.py +606 -0
  44. package/server/tests/test_isolation.py +90 -0
  45. package/server/tests/test_ui_server.py +385 -0
  46. package/server/tests/test_ui_shader_background.py +52 -0
  47. package/server/tests/test_ui_story_6_3.py +105 -0
  48. package/server/tools/__init__.py +0 -0
  49. package/server/tools/_mcp.py +4 -0
  50. package/server/tools/agents.py +160 -0
  51. package/server/tools/ecc/__init__.py +1 -0
  52. package/server/tools/ecc/data.py +35 -0
  53. package/server/tools/ecc/diagrams.py +23 -0
  54. package/server/tools/ecc/pipeline.py +24 -0
  55. package/server/tools/ecc/presentation.py +24 -0
  56. package/server/tools/ecc/web.py +23 -0
  57. package/server/tools/executor.py +880 -0
  58. package/server/tools/harness.py +330 -0
  59. package/server/tools/help.py +162 -0
  60. package/server/tools/hooks.py +357 -0
  61. package/server/tools/personas.py +110 -0
  62. package/server/tools/registry.py +195 -0
  63. package/server/tools/router.py +117 -0
  64. package/server/tools/skill_installer.py +230 -0
  65. package/server/tools/summarizer.py +168 -0
  66. package/server/tools/tests/__init__.py +0 -0
  67. package/server/tools/tests/test_agents.py +246 -0
  68. package/server/tools/tests/test_code_index.py +108 -0
  69. package/server/tools/tests/test_ecc_tools.py +51 -0
  70. package/server/tools/tests/test_executor.py +584 -0
  71. package/server/tools/tests/test_help_agent101.py +149 -0
  72. package/server/tools/tests/test_hooks.py +124 -0
  73. package/server/tools/tests/test_kill_thread.py +125 -0
  74. package/server/tools/tests/test_new_thread_list_threads.py +293 -0
  75. package/server/tools/tests/test_personas.py +52 -0
  76. package/server/tools/tests/test_recall_memory.py +55 -0
  77. package/server/tools/tests/test_registry_client.py +308 -0
  78. package/server/tools/tests/test_router.py +263 -0
  79. package/server/tools/tests/test_skill_installer.py +174 -0
  80. package/server/tools/tests/test_switch_thread.py +163 -0
  81. package/server/tools/tests/test_thread_command_skills.py +54 -0
  82. package/server/tools/tests/test_thread_resolver.py +165 -0
  83. package/server/tools/tests/test_tier1_schema.py +296 -0
  84. package/server/tools/thread_resolver.py +75 -0
  85. package/server/tools/tylor.py +374 -0
  86. package/server/tools/ui.py +38 -0
  87. package/server/ui_server.py +292 -0
  88. package/server/validate.py +237 -0
  89. package/skills/add-skill/SKILL.md +37 -0
  90. package/skills/afk-status/SKILL.md +20 -0
  91. package/skills/bmad/SKILL.md +14 -0
  92. package/skills/help-agent101/SKILL.md +48 -0
  93. package/skills/kill-thread/SKILL.md +35 -0
  94. package/skills/list-threads/SKILL.md +35 -0
  95. package/skills/new-thread/SKILL.md +35 -0
  96. package/skills/recall/SKILL.md +39 -0
  97. package/skills/run/SKILL.md +33 -0
  98. package/skills/set-sandbox/SKILL.md +38 -0
  99. package/skills/switch-thread/SKILL.md +38 -0
  100. package/ui/claude-logo.png +0 -0
  101. package/ui/index.html +1314 -0
package/registry.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.0",
3
+ "skills": [
4
+ {
5
+ "name": "bmad",
6
+ "trigger": "Use when the user wants to create a PRD, review stories, or run BMAD workflows.",
7
+ "trigger_description": "Use when the user wants to create a PRD, review stories, or run BMAD workflows.",
8
+ "keywords": [
9
+ "bmad",
10
+ "create",
11
+ "prd",
12
+ "review",
13
+ "stories",
14
+ "workflows"
15
+ ],
16
+ "tool_count": 1,
17
+ "installed_date": "2026-05-13",
18
+ "source_path": "skills/bmad",
19
+ "module": "server.tools.ecc.web",
20
+ "tools": [
21
+ "web_fetch",
22
+ "web_scrape"
23
+ ]
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,24 @@
1
+ # Copy this file to server/.env and fill in your values
2
+ # Required only for Personal (DynamoDB) storage mode
3
+ # Project JSON mode works with zero configuration
4
+
5
+ # AWS — required for DynamoDB/S3/OpenSearch/Bedrock
6
+ AWS_REGION=us-east-1
7
+ AWS_ACCESS_KEY_ID=
8
+ AWS_SECRET_ACCESS_KEY=
9
+ AWS_PROFILE= # optional: use named AWS profile instead of keys
10
+
11
+ # DynamoDB table names (created by install.sh)
12
+ DYNAMO_TABLE=agent101
13
+
14
+ # Bedrock model IDs
15
+ BEDROCK_REGION=us-east-1
16
+ BEDROCK_OPUS_MODEL=us.anthropic.claude-opus-4-7-20251101-v1:0
17
+
18
+ # OpenSearch (optional — enables semantic memory recall)
19
+ OPENSEARCH_HOST=
20
+ OPENSEARCH_PORT=9200
21
+ OPENSEARCH_INDEX=agent-memories
22
+
23
+ # Token overflow fallback (optional)
24
+ ANTHROPIC_PLATFORM_AWS_API_KEY=
File without changes
@@ -0,0 +1,89 @@
1
+ """
2
+ server/config.py — Agent101 server configuration.
3
+ Reads AWS profile, Bedrock region, and Platform on AWS key.
4
+ Resolution order: env var → ~/.agent101/config.json → .env file → defaults.
5
+ Warns on missing optional config; never crashes on startup.
6
+ """
7
+ from __future__ import annotations
8
+ import json
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+
13
+ try:
14
+ from dotenv import load_dotenv
15
+ load_dotenv(Path(__file__).parent / ".env")
16
+ except ImportError:
17
+ pass # python-dotenv optional at import time; already in requirements.txt
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ CONFIG_PATH = Path.home() / ".agent101" / "config.json"
22
+
23
+
24
+ def _load_file_config() -> dict:
25
+ if CONFIG_PATH.exists():
26
+ try:
27
+ with open(CONFIG_PATH) as f:
28
+ data = json.load(f)
29
+ if not isinstance(data, dict):
30
+ logger.warning("~/.agent101/config.json is not a JSON object — ignoring")
31
+ return {}
32
+ return data
33
+ except (json.JSONDecodeError, OSError):
34
+ logger.warning("~/.agent101/config.json is malformed — ignoring")
35
+ return {}
36
+
37
+
38
+ def _load() -> dict:
39
+ file_cfg = _load_file_config()
40
+
41
+ def _get(env_key: str, file_key: str | None = None, default: str | None = None) -> str | None:
42
+ env_val = os.environ.get(env_key)
43
+ if env_val is not None:
44
+ return env_val or None
45
+ return file_cfg.get(file_key or env_key) or default
46
+
47
+ def _get_any(env_keys: tuple[str, ...], default: str | None = None) -> str | None:
48
+ for env_key in env_keys:
49
+ env_val = os.environ.get(env_key)
50
+ if env_val is not None:
51
+ return env_val or None
52
+ file_val = file_cfg.get(env_key)
53
+ if file_val:
54
+ return file_val
55
+ return default
56
+
57
+ cfg = {
58
+ "aws_profile": _get("AWS_PROFILE"),
59
+ "aws_access_key_id": _get("AWS_ACCESS_KEY_ID"),
60
+ "bedrock_region": _get("BEDROCK_REGION", default="us-east-1"),
61
+ "bedrock_opus_model": _get(
62
+ "BEDROCK_OPUS_MODEL",
63
+ default="us.anthropic.claude-opus-4-7-20251101-v1:0",
64
+ ),
65
+ "platform_key": _get_any(("ANTHROPIC_PLATFORM_AWS_API_KEY", "ANTHROPIC_AWS_API_KEY")),
66
+ "platform_base_url": _get("ANTHROPIC_AWS_BASE_URL"),
67
+ "platform_workspace_id": _get("ANTHROPIC_AWS_WORKSPACE_ID"),
68
+ "dynamo_table": _get("DYNAMO_TABLE", default="agent101"),
69
+ "s3_bucket": _get("S3_BUCKET"),
70
+ "opensearch_host": _get("OPENSEARCH_HOST"),
71
+ "opensearch_port": _get("OPENSEARCH_PORT", default="9200"),
72
+ }
73
+
74
+ # Warn on optional keys that affect runtime features
75
+ if not cfg["platform_key"]:
76
+ logger.warning(
77
+ "ANTHROPIC_PLATFORM_AWS_API_KEY/ANTHROPIC_AWS_API_KEY not set — "
78
+ "token overflow fallback to Claude Platform on AWS is disabled"
79
+ )
80
+ if not cfg["opensearch_host"]:
81
+ logger.warning(
82
+ "OPENSEARCH_HOST not set — "
83
+ "semantic memory recall (recall_memory) will be unavailable until configured"
84
+ )
85
+
86
+ return cfg
87
+
88
+
89
+ config = _load()
package/server/main.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ server/main.py — agent101 FastMCP entry point.
3
+
4
+ Starts both FastMCP (stdio transport) and the aiohttp Thread Visualizer
5
+ server concurrently on a single asyncio event loop.
6
+
7
+ Can be run as a script: python3 server/main.py (from the plugin root)
8
+ or as a module: python3 -m server.main
9
+ """
10
+ from __future__ import annotations
11
+ import asyncio
12
+ import logging
13
+ import os
14
+ import signal
15
+ import sys
16
+
17
+ # Allow running as a standalone script OR as a module.
18
+ # When run as `python3 server/main.py`, __package__ is None and relative
19
+ # imports fail. This block ensures the plugin root is on sys.path so that
20
+ # `from server.tools...` absolute imports work in both modes.
21
+ if __package__ is None or __package__ == "":
22
+ _plugin_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ if _plugin_root not in sys.path:
24
+ sys.path.insert(0, _plugin_root)
25
+
26
+ from server.tools._mcp import mcp # noqa: F401
27
+
28
+ # Register @mcp.tool() decorators (side effects)
29
+ from server.tools import tylor # noqa: F401
30
+ from server.tools import agents # noqa: F401
31
+ from server.tools import registry # noqa: F401
32
+ from server.tools import skill_installer # noqa: F401
33
+ from server.tools import help # noqa: F401
34
+ from server.tools import executor # noqa: F401
35
+ from server.tools import ui # noqa: F401
36
+ from server.tools import harness # noqa: F401 — Agent SDK orchestration
37
+ from server import config # noqa: F401
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ async def _main(ui_only: bool = False) -> None:
43
+ from server.ui_server import start_ui_server
44
+
45
+ runner = await start_ui_server()
46
+
47
+ from server.ui_server import ui_available
48
+ if not ui_available:
49
+ print(
50
+ "Thread UI could not start (port 8765 in use) — MCP tools unaffected",
51
+ file=sys.stderr,
52
+ )
53
+
54
+ loop = asyncio.get_running_loop()
55
+ stop_event = asyncio.Event()
56
+
57
+ def _signal_handler() -> None:
58
+ stop_event.set()
59
+
60
+ for sig in (signal.SIGINT, signal.SIGTERM):
61
+ try:
62
+ loop.add_signal_handler(sig, _signal_handler)
63
+ except NotImplementedError:
64
+ pass
65
+
66
+ if ui_only:
67
+ logger.info("Running in UI-only mode — http://localhost:8765")
68
+ await stop_event.wait()
69
+ else:
70
+ mcp_task = asyncio.create_task(mcp.run_stdio_async())
71
+ stop_task = asyncio.create_task(stop_event.wait())
72
+ done, pending = await asyncio.wait(
73
+ [mcp_task, stop_task],
74
+ return_when=asyncio.FIRST_COMPLETED,
75
+ )
76
+ for task in pending:
77
+ task.cancel()
78
+ try:
79
+ await task
80
+ except (asyncio.CancelledError, Exception):
81
+ pass
82
+
83
+ if runner is not None:
84
+ await runner.cleanup()
85
+
86
+
87
+ if __name__ == "__main__":
88
+ import argparse
89
+ parser = argparse.ArgumentParser()
90
+ parser.add_argument('--ui-only', action='store_true',
91
+ help='Run only the aiohttp visualizer server (no MCP stdio)')
92
+ args = parser.parse_args()
93
+ asyncio.run(_main(ui_only=args.ui_only))
@@ -0,0 +1,15 @@
1
+ # Analyst
2
+
3
+ ## Role Description
4
+
5
+ Research and analysis specialist focused on market context, competitive comparison, evidence synthesis, and decision support.
6
+
7
+ ## Communication Style
8
+
9
+ Structured, evidence-oriented, and concise. Separates facts, assumptions, and recommendations.
10
+
11
+ ## ECC Tool Categories
12
+
13
+ - `ecc/web`
14
+ - `ecc/data`
15
+ - `ecc/diagrams`
@@ -0,0 +1,14 @@
1
+ # CEO
2
+
3
+ ## Role Description
4
+
5
+ Strategic executive persona focused on business direction, prioritization, stakeholder framing, and high-leverage product decisions.
6
+
7
+ ## Communication Style
8
+
9
+ Outcome-driven, decisive, and clear. Frames tradeoffs in terms of impact, risk, and sequencing.
10
+
11
+ ## ECC Tool Categories
12
+
13
+ - `ecc/presentation`
14
+ - `ecc/web`
@@ -0,0 +1,15 @@
1
+ # Code Agent
2
+
3
+ ## Role Description
4
+
5
+ Senior implementation engineer focused on shipping correct code, tests, and maintainable technical changes inside the active thread scope.
6
+
7
+ ## Communication Style
8
+
9
+ Direct, concrete, and implementation-first. States assumptions, changed files, validation commands, and residual risks.
10
+
11
+ ## ECC Tool Categories
12
+
13
+ - `ecc/web`
14
+ - `ecc/data`
15
+ - `ecc/pipeline`
@@ -0,0 +1,14 @@
1
+ # CTO
2
+
3
+ ## Role Description
4
+
5
+ Technical leadership persona focused on architecture, systems tradeoffs, delivery risk, platform strategy, and engineering standards.
6
+
7
+ ## Communication Style
8
+
9
+ Pragmatic, precise, and systems-minded. Connects technical choices to constraints, failure modes, and maintainability.
10
+
11
+ ## ECC Tool Categories
12
+
13
+ - `ecc/diagrams`
14
+ - `ecc/pipeline`
@@ -0,0 +1,260 @@
1
+ """
2
+ Story 1.3: DynamoDB Table & S3 Bucket Provisioning
3
+ Each provision_*() returns (passed: bool, message: str).
4
+ run_all() prints results and returns the count of provisioning errors.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Colour helpers (same as validate.py)
16
+ # ---------------------------------------------------------------------------
17
+ GREEN = "\033[0;32m"
18
+ RED = "\033[0;31m"
19
+ YELLOW = "\033[1;33m"
20
+ NC = "\033[0m"
21
+
22
+ def _ok(msg: str) -> str:
23
+ return f" {GREEN}✓{NC} {msg}"
24
+
25
+ def _fail(msg: str) -> str:
26
+ return f" {RED}✗{NC} {msg}"
27
+
28
+ def _warn(msg: str) -> str:
29
+ return f" {YELLOW}⚠{NC} {msg}"
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Config resolution
34
+ # ---------------------------------------------------------------------------
35
+
36
+ def _resolve_config(plugin_dir: str = "") -> dict:
37
+ """
38
+ Resolve provisioning config.
39
+ Priority: ~/.agent101/config.json → env vars → defaults.
40
+ Default bucket name derived from AWS account ID (guarantees global uniqueness).
41
+ """
42
+ config_path = Path.home() / ".agent101" / "config.json"
43
+ file_config: dict = {}
44
+ if config_path.exists():
45
+ try:
46
+ file_config = json.loads(config_path.read_text())
47
+ except (json.JSONDecodeError, OSError):
48
+ pass
49
+
50
+ table_name = (
51
+ file_config.get("DYNAMO_TABLE")
52
+ or os.environ.get("DYNAMO_TABLE", "")
53
+ or "agent101"
54
+ )
55
+
56
+ region = (
57
+ file_config.get("AWS_REGION")
58
+ or os.environ.get("AWS_REGION")
59
+ or os.environ.get("AWS_DEFAULT_REGION", "")
60
+ or "us-east-1"
61
+ )
62
+
63
+ # Bucket name: config → env → default using account ID
64
+ bucket_name = file_config.get("S3_BUCKET") or os.environ.get("S3_BUCKET", "")
65
+ if not bucket_name:
66
+ try:
67
+ import boto3 # noqa: PLC0415
68
+ account_id = boto3.client("sts").get_caller_identity()["Account"]
69
+ bucket_name = f"agent101-blobs-{account_id}"
70
+ except Exception: # noqa: BLE001
71
+ bucket_name = "agent101-blobs"
72
+
73
+ return {"table_name": table_name, "bucket_name": bucket_name, "region": region}
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # DynamoDB provisioning
78
+ # ---------------------------------------------------------------------------
79
+
80
+ def provision_dynamodb(table_name: str) -> tuple[bool, str]:
81
+ """
82
+ Create DynamoDB table if absent (PAY_PER_REQUEST, PK+SK, PITR).
83
+ Skip + validate schema if present.
84
+ """
85
+ try:
86
+ import boto3 # noqa: PLC0415
87
+ from botocore.exceptions import ClientError # noqa: PLC0415
88
+
89
+ client = boto3.client("dynamodb")
90
+
91
+ # Check if table already exists
92
+ try:
93
+ response = client.describe_table(TableName=table_name)
94
+ # Validate schema compatibility
95
+ key_attrs = {k["AttributeName"] for k in response["Table"]["KeySchema"]}
96
+ if {"PK", "SK"}.issubset(key_attrs):
97
+ # Ensure PITR is enabled even on pre-existing tables
98
+ pitr = client.describe_continuous_backups(TableName=table_name)
99
+ pitr_status = (
100
+ pitr["ContinuousBackupsDescription"]
101
+ ["PointInTimeRecoveryDescription"]
102
+ ["PointInTimeRecoveryStatus"]
103
+ )
104
+ if pitr_status != "ENABLED":
105
+ client.update_continuous_backups(
106
+ TableName=table_name,
107
+ PointInTimeRecoverySpecification={"PointInTimeRecoveryEnabled": True},
108
+ )
109
+ return True, _ok(f"DynamoDB table '{table_name}' already exists — skipping")
110
+ else:
111
+ return True, _warn(
112
+ f"DynamoDB table '{table_name}' exists but schema is incompatible "
113
+ f"(found keys: {key_attrs}). agent101 requires PK+SK. "
114
+ "Proceeding — rename the table in ~/.agent101/config.json if needed."
115
+ )
116
+ except ClientError as e:
117
+ if e.response["Error"]["Code"] != "ResourceNotFoundException":
118
+ raise
119
+
120
+ # Table does not exist — create it
121
+ client.create_table(
122
+ TableName=table_name,
123
+ KeySchema=[
124
+ {"AttributeName": "PK", "KeyType": "HASH"},
125
+ {"AttributeName": "SK", "KeyType": "RANGE"},
126
+ ],
127
+ AttributeDefinitions=[
128
+ {"AttributeName": "PK", "AttributeType": "S"},
129
+ {"AttributeName": "SK", "AttributeType": "S"},
130
+ ],
131
+ BillingMode="PAY_PER_REQUEST",
132
+ )
133
+
134
+ # Wait for ACTIVE state before enabling PITR
135
+ waiter = client.get_waiter("table_exists")
136
+ waiter.wait(
137
+ TableName=table_name,
138
+ WaiterConfig={"Delay": 2, "MaxAttempts": 20},
139
+ )
140
+
141
+ # PITR may not be available immediately after ACTIVE — retry up to 5×
142
+ import time # noqa: PLC0415
143
+ for attempt in range(5):
144
+ try:
145
+ client.update_continuous_backups(
146
+ TableName=table_name,
147
+ PointInTimeRecoverySpecification={"PointInTimeRecoveryEnabled": True},
148
+ )
149
+ break
150
+ except ClientError as e:
151
+ if e.response["Error"]["Code"] == "ContinuousBackupsUnavailableException" and attempt < 4:
152
+ time.sleep(3)
153
+ else:
154
+ raise
155
+
156
+ return True, _ok(f"DynamoDB table '{table_name}' created")
157
+
158
+ except ImportError:
159
+ return False, _fail("DynamoDB provisioning — boto3 not installed")
160
+ except Exception as exc: # noqa: BLE001
161
+ return False, _fail(f"DynamoDB provisioning failed — {exc}\n Fix: check IAM permissions (dynamodb:CreateTable, dynamodb:UpdateContinuousBackups)")
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # S3 provisioning
166
+ # ---------------------------------------------------------------------------
167
+
168
+ def provision_s3(bucket_name: str, region: str = "us-east-1") -> tuple[bool, str]:
169
+ """
170
+ Create S3 bucket if absent with private ACL + block public access.
171
+ Skip if already owned by this account.
172
+ NOTE: us-east-1 must NOT pass CreateBucketConfiguration (boto3 quirk).
173
+ """
174
+ try:
175
+ import boto3 # noqa: PLC0415
176
+ from botocore.exceptions import ClientError # noqa: PLC0415
177
+
178
+ client = boto3.client("s3", region_name=region)
179
+
180
+ try:
181
+ if region == "us-east-1":
182
+ client.create_bucket(Bucket=bucket_name)
183
+ else:
184
+ client.create_bucket(
185
+ Bucket=bucket_name,
186
+ CreateBucketConfiguration={"LocationConstraint": region},
187
+ )
188
+
189
+ # Block all public access (security baseline)
190
+ client.put_public_access_block(
191
+ Bucket=bucket_name,
192
+ PublicAccessBlockConfiguration={
193
+ "BlockPublicAcls": True,
194
+ "IgnorePublicAcls": True,
195
+ "BlockPublicPolicy": True,
196
+ "RestrictPublicBuckets": True,
197
+ },
198
+ )
199
+ return True, _ok(f"S3 bucket '{bucket_name}' created")
200
+
201
+ except ClientError as e:
202
+ code = e.response["Error"]["Code"]
203
+ if code == "BucketAlreadyOwnedByYou":
204
+ return True, _ok(f"S3 bucket '{bucket_name}' already exists — skipping")
205
+ if code == "BucketAlreadyExists":
206
+ return False, _fail(
207
+ f"S3 bucket '{bucket_name}' already exists and is owned by another account.\n"
208
+ " Fix: set a unique S3_BUCKET name in ~/.agent101/config.json"
209
+ )
210
+ raise
211
+
212
+ except ImportError:
213
+ return False, _fail("S3 provisioning — boto3 not installed")
214
+ except Exception as exc: # noqa: BLE001
215
+ return False, _fail(f"S3 provisioning failed — {exc}\n Fix: check IAM permissions (s3:CreateBucket, s3:PutPublicAccessBlock)")
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # run_all — called by install.sh
220
+ # ---------------------------------------------------------------------------
221
+
222
+ def run_all(plugin_dir: str = "") -> int:
223
+ """
224
+ Provision all AWS resources, print results, return error count.
225
+ Caller (install.sh) always exits 0 — this is advisory.
226
+ """
227
+ print(f"\n\033[1mProvisioning AWS resources\033[0m")
228
+
229
+ config = _resolve_config(plugin_dir)
230
+ errors = 0
231
+
232
+ passed, msg = provision_dynamodb(config["table_name"])
233
+ print(msg)
234
+ if not passed:
235
+ errors += 1
236
+
237
+ passed, msg = provision_s3(config["bucket_name"], config["region"])
238
+ print(msg)
239
+ if not passed:
240
+ errors += 1
241
+
242
+ if errors > 0:
243
+ print(
244
+ f"\n {YELLOW}⚠{NC} Provisioning: {errors} resource(s) failed. "
245
+ "Personal mode features require DynamoDB + S3.\n"
246
+ " Fix the errors above then re-run ./install.sh"
247
+ )
248
+
249
+ return errors
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # Entry point — called by install.sh as:
254
+ # python3 "$PLUGIN_DIR/server/provision.py" "$PLUGIN_DIR"
255
+ # Always exits 0 (advisory).
256
+ # ---------------------------------------------------------------------------
257
+ if __name__ == "__main__":
258
+ plugin_dir = sys.argv[1] if len(sys.argv) > 1 else ""
259
+ run_all(plugin_dir)
260
+ sys.exit(0)