cinna-cli 0.4.3__tar.gz → 0.5.0__tar.gz
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.
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/PKG-INFO +3 -1
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/README.md +2 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/local_agent_import/local_agent_import.md +42 -6
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/local_agent_import/local_agent_import_acceptance.md +45 -3
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/local_agent_import/local_agent_import_tech.md +103 -6
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/pyproject.toml +1 -1
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/client.py +9 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/kit_contract.py +7 -2
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/local_import.py +111 -1
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_client.py +15 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_local_import.py +327 -5
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/uv.lock +1 -1
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/.claude/commands/cinna-cli.feature.doc.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/.gitignore +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/LICENSE.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/README.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/account_workspace/account_workspace.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/account_workspace/account_workspace_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/account_workspace/account_workspace_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_addons/agent_addons.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_addons/agent_addons_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_addons/agent_addons_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_api/agent_api.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_api/agent_api_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_api/agent_api_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_management/agent_management.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_management/agent_management_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_management/agent_management_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_schedules/agent_schedules.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_schedules/agent_schedules_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/agent_schedules/agent_schedules_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/bootstrap_onboarding/bootstrap_onboarding.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/bootstrap_onboarding/bootstrap_onboarding_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/bootstrap_onboarding/bootstrap_onboarding_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/delegation/delegation.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/delegation/delegation_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/delegation/delegation_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/doctor/doctor.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/doctor/doctor_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/doctor/doctor_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/git_versioning/git_versioning.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/git_versioning/git_versioning_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/git_versioning/git_versioning_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/improvement_requests/improvement_requests.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/improvement_requests/improvement_requests_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/improvement_requests/improvement_requests_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/live_sync/live_sync.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/live_sync/live_sync_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/live_sync/live_sync_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/mcp_integration/mcp_integration.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/mcp_integration/mcp_integration_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/mcp_integration/mcp_integration_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_chat/remote_chat.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_chat/remote_chat_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_chat/remote_chat_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_exec/remote_exec.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_exec/remote_exec_acceptance.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/remote_exec/remote_exec_tech.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/interface.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/mutagen_capabilities.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/scripts/check_docs_references.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/__init__.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/account.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/auth.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/bootstrap.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/chat.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/cli_version.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/config.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/console.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/context.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/delegation.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/doctor.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/errors.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/git_versioning.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/improve.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/logging.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/main.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/mcp_proxy.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/mutagen_runtime.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/scenarios.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/sync.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/sync_session.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/sync_ssh_shim.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/sync_tui.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/templates/ACCOUNT_CLAUDE.md.template +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/templates/CHAT_TESTING.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/templates/CLAUDE.md.template +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/templates/GIT_VERSIONING.md +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/src/cinna/templates/__init__.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/__init__.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/conftest.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_account.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_auth.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_bootstrap.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_chat.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_cli_version.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_config.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_console.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_context.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_delegation.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_doctor.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_git_versioning.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_improve.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_kit_contract.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_main.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_mutagen_runtime.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_onboarding.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_scenarios.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_sync.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_sync_session.py +0 -0
- {cinna_cli-0.4.3 → cinna_cli-0.5.0}/tests/test_sync_ssh_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cinna-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Local development CLI for Cinna Core agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/opencinna/cinna-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/opencinna/cinna-cli
|
|
@@ -338,6 +338,8 @@ The folder rules come from the kit's versioned contract, `.cinna-kit/layout.json
|
|
|
338
338
|
|
|
339
339
|
Where the folder has been published is recorded in `publications.json`, a **sibling** of `cinna-agent.json` holding one entry per Cinna instance — it cannot live inside the manifest, because each entry records a hash of the exported tree and the manifest is part of that tree. Every step is idempotent (agent by the entry whose `platform_url` matches the instance you are logged into, credentials by name, schedules by name), so a partial import is resumed with `--update` instead of duplicating anything. The publication is recorded **only after the push settled** — `--no-push`, a failed flush, or remaining conflicts leave it unrecorded, and the next run is a plain `--update`. A legacy `cloud` block is migrated into the ledger on the first write and is still read until then, so an older folder's `--update` never creates a second agent. `--dry-run` makes no platform call and writes nothing.
|
|
340
340
|
|
|
341
|
+
The manifest's `runtime.engine` (`opencode`, `claude`, `codex`, …) is sent when the agent is **created**; the platform runs an engine it does not support on OpenCode, and `--update` never changes an existing agent's engine. The plan prints an `Engine:` line, and a `Not carried to the cloud:` line naming the manifest fields present that the cloud has no place for (`runtime.model`, `runtime.complexity`, `runtime.credential`, `runtime.permissions`, unknown `runtime.*` keys, `handovers` — the import sends no handover at all — and `capabilities`). A malformed `runtime` costs a warning, never the import.
|
|
342
|
+
|
|
341
343
|
### `cinna skills list <agent> [--json]`
|
|
342
344
|
|
|
343
345
|
List everything an agent carries beyond its prompt, as one deduplicated list. An **addon** is either an installed plugin or a `skills/<name>/` folder — a `SKILL.md` plus its files that the engine loads on demand. The two overlap (a skill installed from the catalog is *also* a plugin link), and the platform owns the dedupe rule, so this prints the server's projection rather than folding the halves itself: one row per addon, with its kind, source (`marketplace` / `bundle` / `catalog` / `local`), status, name and version.
|
|
@@ -301,6 +301,8 @@ The folder rules come from the kit's versioned contract, `.cinna-kit/layout.json
|
|
|
301
301
|
|
|
302
302
|
Where the folder has been published is recorded in `publications.json`, a **sibling** of `cinna-agent.json` holding one entry per Cinna instance — it cannot live inside the manifest, because each entry records a hash of the exported tree and the manifest is part of that tree. Every step is idempotent (agent by the entry whose `platform_url` matches the instance you are logged into, credentials by name, schedules by name), so a partial import is resumed with `--update` instead of duplicating anything. The publication is recorded **only after the push settled** — `--no-push`, a failed flush, or remaining conflicts leave it unrecorded, and the next run is a plain `--update`. A legacy `cloud` block is migrated into the ledger on the first write and is still read until then, so an older folder's `--update` never creates a second agent. `--dry-run` makes no platform call and writes nothing.
|
|
303
303
|
|
|
304
|
+
The manifest's `runtime.engine` (`opencode`, `claude`, `codex`, …) is sent when the agent is **created**; the platform runs an engine it does not support on OpenCode, and `--update` never changes an existing agent's engine. The plan prints an `Engine:` line, and a `Not carried to the cloud:` line naming the manifest fields present that the cloud has no place for (`runtime.model`, `runtime.complexity`, `runtime.credential`, `runtime.permissions`, unknown `runtime.*` keys, `handovers` — the import sends no handover at all — and `capabilities`). A malformed `runtime` costs a warning, never the import.
|
|
305
|
+
|
|
304
306
|
### `cinna skills list <agent> [--json]`
|
|
305
307
|
|
|
306
308
|
List everything an agent carries beyond its prompt, as one deduplicated list. An **addon** is either an installed plugin or a `skills/<name>/` folder — a `SKILL.md` plus its files that the engine loads on demand. The two overlap (a skill installed from the catalog is *also* a plugin link), and the platform owns the dedupe rule, so this prints the server's projection rather than folding the halves itself: one row per addon, with its kind, source (`marketplace` / `bundle` / `catalog` / `local`), status, name and version.
|
|
@@ -21,7 +21,9 @@ The kit's machine-readable half is a **versioned contract** (`layout.json`,
|
|
|
21
21
|
`GET /api/agent-start/contract.tar.gz` with a cheap version poll at
|
|
22
22
|
`GET /api/agent-start/contract/version`. cinna-cli reads the copy installed at
|
|
23
23
|
`.cinna-kit/`; a shipped build should obtain it from those routes rather than
|
|
24
|
-
vendoring one.
|
|
24
|
+
vendoring one. This build implements contract **1.5.0** (`runtime.engine`,
|
|
25
|
+
`runtime.complexity` and `handovers[].target_kind` in core's one schema); only
|
|
26
|
+
the major decides compatibility, so any `1.x` folder imports.
|
|
25
27
|
|
|
26
28
|
Nothing platform-side is new: the import replays the manifest through the
|
|
27
29
|
existing account-scoped verbs (`agent create`, the bulk prompt write, `agent
|
|
@@ -57,6 +59,30 @@ sync`, `sync push`, `account credentials create`, `agent schedule create`,
|
|
|
57
59
|
`.env.sample` and `.env.template` still travel. Credentials are created as
|
|
58
60
|
**empty drafts** and the command prints the URLs the user opens to fill each
|
|
59
61
|
one in the browser. No secret value is read, sent, or printed at any point.
|
|
62
|
+
- **A credential spec can name its slot.** Since contract 1.4.0 a manifest
|
|
63
|
+
credential may carry `service_uri` — the slot the agent's credential reader
|
|
64
|
+
matches an attached credential on. The import passes it to the draft
|
|
65
|
+
unchanged; one that is not a non-empty string is refused with the other
|
|
66
|
+
manifest guards, before the agent is created.
|
|
67
|
+
- **The engine is a preference, carried once.** A manifest's `runtime.engine`
|
|
68
|
+
(`opencode`, `claude`, `codex`, or a name this build has never heard of) is
|
|
69
|
+
sent to the platform when the agent is **created**, which maps it to the
|
|
70
|
+
environment's SDK and runs an engine it does not support on OpenCode — the
|
|
71
|
+
folder is never refused over it. `--update` never changes an existing agent's
|
|
72
|
+
engine. A `runtime` that is not an object, or an engine that is not a string,
|
|
73
|
+
costs a warning and nothing is sent.
|
|
74
|
+
- **What the cloud does not carry is named, not dropped silently.** The other
|
|
75
|
+
`runtime` fields (`model`, `complexity`, `credential`, `permissions`, and any
|
|
76
|
+
key this build does not know) have no cloud counterpart, and the import
|
|
77
|
+
sends no `handovers` at all (not only the desktop's `target_kind`) and maps
|
|
78
|
+
`capabilities` onto nothing; the plan lists the ones present in one
|
|
79
|
+
`Not carried to the cloud:` line.
|
|
80
|
+
- **Using cloud credentials on the desktop is not an import concern.** A
|
|
81
|
+
locally running agent in Cinna Desktop gets credential values from the
|
|
82
|
+
platform's desktop delivery route, gated per credential by *allow local use*
|
|
83
|
+
for credentials shared with you (your own always qualify). That exchange is
|
|
84
|
+
between the platform and the desktop; `cinna agent import` never reads
|
|
85
|
+
credential values, and the CLI has no switch for *allow local use* yet.
|
|
60
86
|
- **The ledger is a sibling file, never a manifest key.** Where an agent folder
|
|
61
87
|
has been published is recorded in `publications.json` beside
|
|
62
88
|
`cinna-agent.json` — one entry per Cinna instance. It has to be a separate
|
|
@@ -83,10 +109,11 @@ Each prints as `[n/9]`:
|
|
|
83
109
|
contract: refuse a folder built against a newer **major** contract version,
|
|
84
110
|
warn on an older one or on a folder that records none, resolve the exclude
|
|
85
111
|
list and the secret rules, plan the copy and compute the tree's
|
|
86
|
-
`content_hash`.
|
|
112
|
+
`content_hash`. Read `runtime` tolerantly and print the `Engine:` line and,
|
|
113
|
+
when any are present, the `Not carried to the cloud:` line.
|
|
87
114
|
2. **Agent** — resolve by the `publications.json` entry for this instance
|
|
88
115
|
(requires `--update`) or create a new agent in the active user workspace
|
|
89
|
-
(`--workspace` overrides).
|
|
116
|
+
(`--workspace` overrides), passing `runtime.engine` on creation only.
|
|
90
117
|
3. **Prompts + metadata** — one bulk write carrying `description`,
|
|
91
118
|
`router_trigger_prompt`, `example_prompts`, and the three document prompts
|
|
92
119
|
read from the files named in `prompts`; then the status refresh command.
|
|
@@ -99,7 +126,8 @@ Each prints as `[n/9]`:
|
|
|
99
126
|
6. **Push** — `cinna sync push` equivalent (skipped by `--no-push`).
|
|
100
127
|
7. **Credentials** — one empty draft per spec, attached to the agent; an
|
|
101
128
|
existing credential of the same name is attached rather than recreated.
|
|
102
|
-
|
|
129
|
+
A spec's `service_uri` is set on a new draft; an existing credential keeps
|
|
130
|
+
its own. Setup URLs are collected for the summary.
|
|
103
131
|
8. **Schedules** — one per spec, name-idempotent; `--update` rewrites an
|
|
104
132
|
existing schedule in place.
|
|
105
133
|
9. **Record** — add or update this instance's entry in `publications.json`
|
|
@@ -131,7 +159,11 @@ file that would be copied, every credential draft, every schedule — and makes
|
|
|
131
159
|
`publications.json` entry whose `platform_url` matches the instance you are
|
|
132
160
|
logged into (not by workspace, and not by position in the list), rewrites
|
|
133
161
|
prompts and metadata, re-copies the tree, pushes, attaches existing credentials,
|
|
134
|
-
and updates the schedules in place.
|
|
162
|
+
and updates the schedules in place. The agent's engine is left as it is; when
|
|
163
|
+
the manifest names one, the `Engine:` line says it was not applied. An
|
|
164
|
+
`--update` with no known agent id cannot tell from the plan whether it will
|
|
165
|
+
find the agent by name or create it, so the plan says the engine is sent only
|
|
166
|
+
on creation, and the lookup then says which happened.
|
|
135
167
|
|
|
136
168
|
### Resume a partial import
|
|
137
169
|
|
|
@@ -160,11 +192,15 @@ created are reused, not duplicated.
|
|
|
160
192
|
| `schema_version` newer than supported | Refused with an upgrade hint — never half-read |
|
|
161
193
|
| Folder built against a newer **major** contract version | Refused with an upgrade hint — a minor or patch difference is not a compatibility question and passes silently |
|
|
162
194
|
| Folder records no `contract_version` | Warned and imported — every folder created before contract 1.0.0 is in that state |
|
|
195
|
+
| A credential's `service_uri` is empty or not a string | Refused before any platform call, naming the credential |
|
|
196
|
+
| `runtime` is not an object, or `runtime.engine` is not a string | Warned and imported; no engine is sent, so the agent gets the platform default |
|
|
197
|
+
| `runtime.engine` names an engine the cloud does not support | Imported; the platform runs the agent on OpenCode |
|
|
198
|
+
| Older platform without the `engine` create field | Imported; the platform ignores the unknown field and the agent gets its default engine |
|
|
163
199
|
| Already imported to this instance, no `--update` | Refused, naming the agent id and where the link was recorded |
|
|
164
200
|
| The recorded agent id is not among your agents | Refused — you are logged into a different platform, or the agent was deleted |
|
|
165
201
|
| `--update` with several name matches | Refused, listing them; add the `publications.json` entry to disambiguate |
|
|
166
202
|
| Push failed or left conflicts | Warned, **nothing recorded**, with the resolve + `--update` hint |
|
|
167
|
-
| `.cinna-kit/layout.json` missing or unreadable | Imported with the built-in contract
|
|
203
|
+
| `.cinna-kit/layout.json` missing or unreadable | Imported with the built-in copy of contract 1.5.0, the `Exclusions:` line marked `DEGRADED`, and **no `content_hash` recorded** — a hash over a file set another host would not select is worse than none |
|
|
168
204
|
| A travelling file or directory cannot be read | Refused — a complete-looking export with a subtree missing from it is the one outcome worth stopping for |
|
|
169
205
|
| `publications.json` unreadable | Refused, and the file is left exactly as it was |
|
|
170
206
|
| Credential/schedule 403 | The platform error surfaces verbatim; earlier steps stay done and `--update` resumes |
|
|
@@ -121,7 +121,8 @@ any change to the orchestrator.
|
|
|
121
121
|
|
|
122
122
|
- **Steps:** in a scratch copy of the agent folder, one at a time: bump
|
|
123
123
|
`schema_version` to `2`; rename the folder so it disagrees with `slug`; make
|
|
124
|
-
`cron_string` `"every morning"`; drop a credential's `type
|
|
124
|
+
`cron_string` `"every morning"`; drop a credential's `type`; set a
|
|
125
|
+
credential's `service_uri` to `""`.
|
|
125
126
|
- **Expected:** each is refused *before* any platform call, with a message that
|
|
126
127
|
names the offending field.
|
|
127
128
|
- **Watch for:** a half-applied import (agent created, then the manifest
|
|
@@ -242,14 +243,55 @@ any change to the orchestrator.
|
|
|
242
243
|
### 10f. The `contract_version` gate
|
|
243
244
|
|
|
244
245
|
- **Steps:** set the manifest's `contract_version` to `2.0.0` and import; then
|
|
245
|
-
to `1.
|
|
246
|
+
to `1.5.2` and import; then remove the key entirely and import.
|
|
246
247
|
- **Expected:** `2.0.0` is refused before any platform call, naming `2.x`;
|
|
247
|
-
`1.
|
|
248
|
+
`1.5.2` passes **silently** (same major — a minor contract change is additive
|
|
248
249
|
by definition and reaches a non-adopting reader as silence); the missing key
|
|
249
250
|
warns about a re-stamp and imports anyway.
|
|
250
251
|
- **Watch for:** a warning on the same-major pair. That is the gate's shape, not
|
|
251
252
|
a bug: anything an old reader must notice needs a major bump.
|
|
252
253
|
|
|
254
|
+
### 10g. A credential slot reaches the draft (contract 1.4.0)
|
|
255
|
+
|
|
256
|
+
- **Goal:** a manifest credential that names its slot gets a draft the agent's
|
|
257
|
+
credential reader can match, in the cloud and on the desktop.
|
|
258
|
+
- **Setup:** a fresh agent whose manifest credential carries
|
|
259
|
+
`"service_uri": "imap://billing"` and whose name does **not** exist yet in the
|
|
260
|
+
account.
|
|
261
|
+
- **Steps:**
|
|
262
|
+
```bash
|
|
263
|
+
cinna agent import MyAgents/Local/<agent> --yes
|
|
264
|
+
cinna account credentials list
|
|
265
|
+
```
|
|
266
|
+
- **Expected:** the draft is listed with `imap://billing` in the **Slot** column; after it is filled
|
|
267
|
+
in the browser, `cinna skills list <agent>` (if a skill declares the slot)
|
|
268
|
+
shows it ready. Re-running with `--update` does not change the URI of the
|
|
269
|
+
existing credential.
|
|
270
|
+
- **Watch for:** a draft created without the slot, so the reader falls back to
|
|
271
|
+
name matching; a second draft created because the slot differs.
|
|
272
|
+
|
|
273
|
+
### 10h. The engine travels at creation, and nothing else from `runtime` (contract 1.5.0)
|
|
274
|
+
|
|
275
|
+
- **Setup:** a fresh agent whose manifest carries
|
|
276
|
+
`"runtime": {"engine": "claude", "model": "claude-sonnet", "complexity": "medium"}`
|
|
277
|
+
and a handover with `"target_kind": "coordinator"`. Requires a cinna-core that
|
|
278
|
+
accepts `engine` on `POST /api/v1/cli/account/agents`.
|
|
279
|
+
- **Steps:**
|
|
280
|
+
```bash
|
|
281
|
+
cinna agent import MyAgents/Local/<agent> --dry-run
|
|
282
|
+
cinna agent import MyAgents/Local/<agent> --yes
|
|
283
|
+
cinna agent import MyAgents/Local/<agent> --update --yes
|
|
284
|
+
```
|
|
285
|
+
- **Expected:** both the dry run and the import print
|
|
286
|
+
`Engine: claude — sent as a preference (the cloud runs engines it does not support on OpenCode)`
|
|
287
|
+
and `Not carried to the cloud: runtime.model, runtime.complexity, handovers`;
|
|
288
|
+
the new agent's environment runs on Claude Code. Set `"engine": "codex"` on a
|
|
289
|
+
fresh agent: it imports and runs on OpenCode. The `--update` run prints
|
|
290
|
+
`Engine: claude — not applied: --update never changes an existing agent's engine`
|
|
291
|
+
and the environment is unchanged.
|
|
292
|
+
- **Watch for:** the import refusing an unknown engine; the model override
|
|
293
|
+
changing; an older cinna-core rejecting the create (it should ignore `engine`).
|
|
294
|
+
|
|
253
295
|
### 11. Verification loop the guide promises
|
|
254
296
|
|
|
255
297
|
- **Steps:**
|
{cinna_cli-0.4.3 → cinna_cli-0.5.0}/docs/features/local_agent_import/local_agent_import_tech.md
RENAMED
|
@@ -39,7 +39,7 @@ endpoint exists for this feature.
|
|
|
39
39
|
|
|
40
40
|
| Step | Call |
|
|
41
41
|
|------|------|
|
|
42
|
-
| 2 | `list_account_agents()` (resolve) / `create_agent(name, description, user_workspace_id=…)` |
|
|
42
|
+
| 2 | `list_account_agents()` (resolve) / `create_agent(name, description, user_workspace_id=…, engine=…)` — `engine` is in the body only when set |
|
|
43
43
|
| 3 | `update_agent_config(agent_id, fields)` → `PUT agents/{id}` **through the api-proxy escape hatch**, then `set_status_refresh_command(agent_id, cmd)` |
|
|
44
44
|
| 4 | `account.py:run_agent_sync(agent_id, None)` (mints the child token, provisions the workspace) — skipped when `resolve_child_workspace()` already finds one |
|
|
45
45
|
| 6 | `sync_session.ensure_session()` + `sync_session.flush()` — the same pair `cinna sync push` uses |
|
|
@@ -70,10 +70,59 @@ pragmatic subset `kit.py validate` checks:
|
|
|
70
70
|
- `slug` matches `^[a-z0-9][a-z0-9-]{1,62}$` **and** equals the folder name.
|
|
71
71
|
- `name` present, ≤ 255 chars; `description` a string when present.
|
|
72
72
|
- `prompts` an object; `example_prompts` a list of strings.
|
|
73
|
-
- Each credential has a non-empty `name` and `type
|
|
73
|
+
- Each credential has a non-empty `name` and `type`, and a `service_uri`
|
|
74
|
+
(contract 1.4.0, optional) is a non-empty string when present. Checked here
|
|
75
|
+
rather than left to the platform because the agent already exists by the time
|
|
76
|
+
`_sync_credentials()` sends it — a 422 there is a half-applied import.
|
|
74
77
|
- Each schedule has a name, a 5-field `cron_string`, a known `schedule_type`, and
|
|
75
78
|
a `command` when it is a script schedule.
|
|
76
79
|
|
|
80
|
+
`runtime` is **not** validated here — `read_runtime(manifest)` reads it
|
|
81
|
+
tolerantly after the manifest loads, because its fields are additive minors
|
|
82
|
+
that must never cost the import:
|
|
83
|
+
|
|
84
|
+
- `runtime` not an object, or `runtime.engine` not a string → a
|
|
85
|
+
`console.warn` naming the type, and no engine is sent.
|
|
86
|
+
- `runtime.engine` stripped; empty means absent. Otherwise sent raw — it is not
|
|
87
|
+
an enum, and the backend owns the mapping (`claude` → Claude Code, everything
|
|
88
|
+
it does not support → OpenCode, decision 4 of the 1.5.0 unification plan).
|
|
89
|
+
- The fields the cloud import does not carry are returned as dotted paths, in
|
|
90
|
+
a fixed order: `runtime.model`, `runtime.complexity`, `runtime.credential`,
|
|
91
|
+
`runtime.permissions` (`_UNCARRIED_RUNTIME_KEYS`), then any unknown
|
|
92
|
+
`runtime.*` key sorted, then `handovers` — the whole list, when it is
|
|
93
|
+
non-empty, because this import sends no handover at all; naming only
|
|
94
|
+
`handovers[].target_kind` would imply the rest of a handover travels — then
|
|
95
|
+
`capabilities`, when any of its keys has a value (`x-import: ignored`). A
|
|
96
|
+
key whose value is `null` counts as absent.
|
|
97
|
+
|
|
98
|
+
The engine reaches `_resolve_or_create_agent()` and is passed to
|
|
99
|
+
`create_agent()` **only on the create branch**. An agent resolved by id or
|
|
100
|
+
reattached by name keeps its engine: the id case says so in the step-1 `Engine:`
|
|
101
|
+
line (`known_agent_id` is known before the plan prints). `--update` without a
|
|
102
|
+
known id only learns at step 2 whether a name match exists, so its step-1 line
|
|
103
|
+
is the undecided one, and step 2 prints either the kept line (name-matched
|
|
104
|
+
resume) or the sent line (no match, agent created). The `Engine:` values,
|
|
105
|
+
verbatim:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Engine: claude — sent as a preference (the cloud runs engines it does not support on OpenCode)
|
|
109
|
+
Engine: claude — not applied: --update never changes an existing agent's engine
|
|
110
|
+
Engine: claude — sent only if this import creates the agent; --update keeps the engine of an existing agent it finds by name
|
|
111
|
+
Engine: not set — the cloud agent gets the platform's default
|
|
112
|
+
Not carried to the cloud: runtime.model, runtime.complexity, handovers
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The last line is printed only when the list is non-empty. The warnings:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
! cinna-agent.json: 'runtime' is not an object (str) — ignored; the cloud agent gets the platform's default engine.
|
|
119
|
+
! cinna-agent.json: 'runtime.engine' is not a string (int) — ignored; the cloud agent gets the platform's default engine.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Sending `engine` to an older cinna-core is safe: `AccountAgentCreateBody` is a
|
|
123
|
+
plain `SQLModel` with no `extra` setting, so pydantic's default (`ignore`)
|
|
124
|
+
drops the unknown field and the agent is created with the default engine.
|
|
125
|
+
|
|
77
126
|
Unknown keys are preserved: the dict is mutated and re-dumped, so a manifest
|
|
78
127
|
written by a newer kit survives a round-trip through an older CLI.
|
|
79
128
|
|
|
@@ -91,7 +140,39 @@ hosts read it — Cinna Desktop, cinna-core's `kit.py`, and this CLI — so the
|
|
|
91
140
|
folder rules are data rather than prose each host re-derives.
|
|
92
141
|
`src/cinna/kit_contract.py` is cinna-cli's reader and the port of the algorithms
|
|
93
142
|
that must stay byte-identical across hosts. `SUPPORTED_CONTRACT_VERSION` names
|
|
94
|
-
the contract version this build implements.
|
|
143
|
+
the contract version this build implements — **1.5.0**. It is the tool half of
|
|
144
|
+
the compatibility pair when no kit sits beside the agent, and it is the version
|
|
145
|
+
named in the `DEGRADED —` `Exclusions:` line.
|
|
146
|
+
|
|
147
|
+
### Contract 1.5.0 — what changed for this reader
|
|
148
|
+
|
|
149
|
+
1.5.0 brings the desktop's `runtime.engine`, `runtime.complexity` and
|
|
150
|
+
`handovers[].target_kind` into cinna-core's one manifest schema. Nothing this
|
|
151
|
+
module evaluates changed — `cloud_import_excludes`, `secret_files` and the
|
|
152
|
+
ledger schema are as in 1.4.0. The manifest side is `local_import.py`:
|
|
153
|
+
`runtime.engine` now reaches the platform at creation, and the fields the cloud
|
|
154
|
+
does not carry are printed (see *Manifest handling*).
|
|
155
|
+
|
|
156
|
+
### Contract 1.4.0 — what changed for this reader
|
|
157
|
+
|
|
158
|
+
1.4.0 (cloud and local credential delivery) is additive for cinna-cli; the
|
|
159
|
+
cross-repo checks below pass unchanged against cinna-core's 1.4.0 files.
|
|
160
|
+
|
|
161
|
+
- `cloud_import_excludes` is unchanged in content: `credentials/` was already
|
|
162
|
+
excluded whole, and the notes now say why (the platform regenerates
|
|
163
|
+
`credentials/README.md` and feeds it to the cloud prompt). `DEFAULT_EXCLUDE`
|
|
164
|
+
and `DEFAULT_SECRET_FILE_RULES` equal the 1.4.0 lists.
|
|
165
|
+
- `desktop_owned` shrank to a bare path list (the `contract_keys` block is
|
|
166
|
+
gone). This module does not read `desktop_owned`, so nothing depends on it.
|
|
167
|
+
- The manifest schema gained an optional `credentials[].service_uri`;
|
|
168
|
+
`local_import.py:load_manifest()` validates it and
|
|
169
|
+
`local_import.py:_sync_credentials()` passes it to
|
|
170
|
+
`client.py:AccountClient.create_credential()`.
|
|
171
|
+
- Credential values for a locally running agent come from cinna-core's
|
|
172
|
+
`/api/v1/external/credentials` routes (list, and a desktop-only
|
|
173
|
+
`materialize`), gated per credential by `allow_local_use`. Those are
|
|
174
|
+
desktop-JWT routes; `POST|PUT /api/v1/cli/account/credentials` do not accept
|
|
175
|
+
`allow_local_use` yet, so the CLI neither sends nor shows it.
|
|
95
176
|
|
|
96
177
|
### Finding it
|
|
97
178
|
|
|
@@ -293,7 +374,9 @@ failure warns and yields no file rather than aborting the import.
|
|
|
293
374
|
absent + no `--update` → create.
|
|
294
375
|
- **Credentials** — `list_credentials()` keyed by name; an existing name is only
|
|
295
376
|
*attached* (`share_credential_with_agent`), never recreated, so re-runs cannot
|
|
296
|
-
produce a second empty draft that shadows the filled one.
|
|
377
|
+
produce a second empty draft that shadows the filled one. Its `service_uri`
|
|
378
|
+
is not compared or rewritten either — the manifest's slot applies to a new
|
|
379
|
+
draft only.
|
|
297
380
|
- **Schedules** — `list_schedules()` keyed by name; existing + `--update` →
|
|
298
381
|
`update_schedule`, existing without `--update` → left alone with a notice.
|
|
299
382
|
- **Manifest settle** — `write_manifest()` runs after the confirmation and
|
|
@@ -316,7 +399,8 @@ failure warns and yields no file rather than aborting the import.
|
|
|
316
399
|
|
|
317
400
|
`--dry-run` returns before the first `AccountClient` is constructed and before
|
|
318
401
|
any local write: it prints the resolved plan (file list, credential drafts,
|
|
319
|
-
schedules,
|
|
402
|
+
schedules, the `Engine:` and `Not carried to the cloud:` lines,
|
|
403
|
+
create-vs-update) and stops. The test asserts the patched
|
|
320
404
|
`AccountClient` class was never called at all.
|
|
321
405
|
|
|
322
406
|
## Tests
|
|
@@ -386,4 +470,17 @@ against `publications.schema.json`.
|
|
|
386
470
|
- `run_agent_sync` is called exactly once when no workspace exists yet;
|
|
387
471
|
- `--no-push` and push-with-conflicts both leave the ledger unwritten;
|
|
388
472
|
- `layout.json` supplying the exclude list while `credentials/` stays excluded;
|
|
389
|
-
-
|
|
473
|
+
- a bad credential `service_uri` refused by `load_manifest()`, and a good one
|
|
474
|
+
reaching `create_credential()`;
|
|
475
|
+
- `--workspace` / `--name` threading and verbatim `PlatformError` surfacing;
|
|
476
|
+
- contract 1.5.0 runtime: `read_runtime()` over absent, null, blank, unknown and
|
|
477
|
+
full `runtime` blocks and empty/non-empty `handovers`; the stripped engine
|
|
478
|
+
reaching `create_agent()`; the default-engine line; a non-object `runtime`
|
|
479
|
+
and a non-string engine warning without refusing; the exact
|
|
480
|
+
`Not carried to the cloud:` line on a real run and on `--dry-run`, with no
|
|
481
|
+
runtime field in the metadata write; `--update` by id and by name match
|
|
482
|
+
leaving the engine alone and saying so once, and saying nothing when the
|
|
483
|
+
manifest names no engine.
|
|
484
|
+
|
|
485
|
+
`tests/test_client.py` asserts `create_agent()` puts `engine` in the body only
|
|
486
|
+
when it is set.
|
|
@@ -318,6 +318,7 @@ class AccountClient:
|
|
|
318
318
|
name: str,
|
|
319
319
|
description: str | None = None,
|
|
320
320
|
user_workspace_id: str | None = None,
|
|
321
|
+
engine: str | None = None,
|
|
321
322
|
) -> dict:
|
|
322
323
|
"""POST /api/v1/cli/account/agents — create an agent (thin client).
|
|
323
324
|
|
|
@@ -325,12 +326,20 @@ class AccountClient:
|
|
|
325
326
|
(AI credentials, env template, environment creation) and returns the
|
|
326
327
|
full agent record. ``user_workspace_id`` targets the account's active
|
|
327
328
|
user workspace (``None`` = Default).
|
|
329
|
+
|
|
330
|
+
``engine`` is a Local Agent Kit ``runtime.engine`` preference
|
|
331
|
+
(``opencode``, ``claude``, ``codex``, …), sent raw: the backend maps it
|
|
332
|
+
to the environment's SDK and runs an engine it does not support on
|
|
333
|
+
OpenCode. It is sent only when set — an older backend ignores the
|
|
334
|
+
unknown field, so sending it is backward compatible.
|
|
328
335
|
"""
|
|
329
336
|
body: dict = {"name": name}
|
|
330
337
|
if description is not None:
|
|
331
338
|
body["description"] = description
|
|
332
339
|
if user_workspace_id is not None:
|
|
333
340
|
body["user_workspace_id"] = user_workspace_id
|
|
341
|
+
if engine is not None:
|
|
342
|
+
body["engine"] = engine
|
|
334
343
|
response = self._client.post("/api/v1/cli/account/agents", json=body)
|
|
335
344
|
return self._handle_response(response).json()
|
|
336
345
|
|
|
@@ -24,7 +24,12 @@ rather than compute it a different way. The safe direction is a property of the
|
|
|
24
24
|
consequence, not a house style.
|
|
25
25
|
|
|
26
26
|
Ported from cinna-core's ``docs/local_agent_kit/tools/kit.py``, which is the
|
|
27
|
-
reference implementation, at contract version 1.0.0
|
|
27
|
+
reference implementation, at contract version 1.0.0; the offline fallbacks below
|
|
28
|
+
are checked against contract 1.5.0. Neither 1.4.0 (cloud and local credential
|
|
29
|
+
delivery — ``credentials/`` was already excluded whole) nor 1.5.0 (the
|
|
30
|
+
``runtime.engine``/``runtime.complexity``/``handovers[].target_kind`` manifest
|
|
31
|
+
fields brought into core's schema, read by :mod:`cinna.local_import`) changed
|
|
32
|
+
anything this module evaluates.
|
|
28
33
|
"""
|
|
29
34
|
|
|
30
35
|
from __future__ import annotations
|
|
@@ -54,7 +59,7 @@ CONTRACT_VERSION_FILENAME = "CONTRACT_VERSION"
|
|
|
54
59
|
|
|
55
60
|
#: The contract version this CLI implements. Only the MAJOR component is a
|
|
56
61
|
#: compatibility question — see ``check_contract_compatibility``.
|
|
57
|
-
SUPPORTED_CONTRACT_VERSION = "1.
|
|
62
|
+
SUPPORTED_CONTRACT_VERSION = "1.5.0"
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
# ── Offline fallbacks ───────────────────────────────────────────────────────
|
|
@@ -181,6 +181,16 @@ def load_manifest(source: Path) -> dict:
|
|
|
181
181
|
raise click.ClickException(
|
|
182
182
|
f"{MANIFEST_FILENAME}: credential '{cred.get('name')}' needs a 'type'."
|
|
183
183
|
)
|
|
184
|
+
# Contract 1.4.0: an optional explicit slot. Checked here because the
|
|
185
|
+
# agent already exists by the time the credential draft is created.
|
|
186
|
+
service_uri = cred.get("service_uri")
|
|
187
|
+
if service_uri is not None and (
|
|
188
|
+
not isinstance(service_uri, str) or not service_uri.strip()
|
|
189
|
+
):
|
|
190
|
+
raise click.ClickException(
|
|
191
|
+
f"{MANIFEST_FILENAME}: credential '{cred['name']}' has a "
|
|
192
|
+
f"'service_uri' that is not a non-empty string."
|
|
193
|
+
)
|
|
184
194
|
|
|
185
195
|
for sched in _manifest_list(data, "schedules"):
|
|
186
196
|
if not isinstance(sched.get("name"), str) or not sched["name"].strip():
|
|
@@ -217,6 +227,67 @@ def _manifest_list(data: dict, key: str) -> list[dict]:
|
|
|
217
227
|
return value
|
|
218
228
|
|
|
219
229
|
|
|
230
|
+
#: ``runtime.*`` keys the contract names that the cloud import does not carry
|
|
231
|
+
#: (contract 1.5.0, ``x-import: ignored``), in the order they are reported.
|
|
232
|
+
#: ``runtime.engine`` is the one runtime field that travels — at creation only.
|
|
233
|
+
_UNCARRIED_RUNTIME_KEYS = ("model", "complexity", "credential", "permissions")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def read_runtime(manifest: dict) -> tuple[str | None, list[str]]:
|
|
237
|
+
"""``(engine to send, manifest fields the cloud does not carry)``.
|
|
238
|
+
|
|
239
|
+
Read tolerantly, never refused: ``runtime`` is optional and its fields are
|
|
240
|
+
additive minors, so a malformed value costs a warning and nothing is sent —
|
|
241
|
+
the cloud agent then gets the platform's default engine.
|
|
242
|
+
|
|
243
|
+
The engine is a *preference*, sent stripped and otherwise raw; the backend
|
|
244
|
+
owns the mapping (``claude`` → Claude Code, anything it does not support →
|
|
245
|
+
OpenCode). The not-carried list names every field present with a value that
|
|
246
|
+
this import drops on the floor, so the loss is printed rather than silent:
|
|
247
|
+
the known ``runtime`` keys, any ``runtime`` key this build does not know,
|
|
248
|
+
``handovers`` — whole, because this import never sends a handover at all
|
|
249
|
+
(naming only ``target_kind`` would imply the rest of one travels) — and
|
|
250
|
+
``capabilities``, which the import maps onto nothing.
|
|
251
|
+
"""
|
|
252
|
+
not_carried: list[str] = []
|
|
253
|
+
engine: str | None = None
|
|
254
|
+
|
|
255
|
+
runtime = manifest.get("runtime")
|
|
256
|
+
if runtime is not None and not isinstance(runtime, dict):
|
|
257
|
+
console.warn(
|
|
258
|
+
f"{MANIFEST_FILENAME}: 'runtime' is not an object "
|
|
259
|
+
f"({type(runtime).__name__}) — ignored; the cloud agent gets the "
|
|
260
|
+
f"platform's default engine."
|
|
261
|
+
)
|
|
262
|
+
elif isinstance(runtime, dict):
|
|
263
|
+
raw = runtime.get("engine")
|
|
264
|
+
if isinstance(raw, str):
|
|
265
|
+
engine = raw.strip() or None
|
|
266
|
+
elif raw is not None:
|
|
267
|
+
console.warn(
|
|
268
|
+
f"{MANIFEST_FILENAME}: 'runtime.engine' is not a string "
|
|
269
|
+
f"({type(raw).__name__}) — ignored; the cloud agent gets the "
|
|
270
|
+
f"platform's default engine."
|
|
271
|
+
)
|
|
272
|
+
present = [k for k, v in runtime.items() if k != "engine" and v is not None]
|
|
273
|
+
not_carried += [f"runtime.{k}" for k in _UNCARRIED_RUNTIME_KEYS if k in present]
|
|
274
|
+
not_carried += [
|
|
275
|
+
f"runtime.{k}" for k in sorted(present) if k not in _UNCARRIED_RUNTIME_KEYS
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
handovers = manifest.get("handovers")
|
|
279
|
+
if isinstance(handovers, list) and handovers:
|
|
280
|
+
not_carried.append("handovers")
|
|
281
|
+
|
|
282
|
+
capabilities = manifest.get("capabilities")
|
|
283
|
+
if isinstance(capabilities, dict) and any(
|
|
284
|
+
v is not None for v in capabilities.values()
|
|
285
|
+
):
|
|
286
|
+
not_carried.append("capabilities")
|
|
287
|
+
|
|
288
|
+
return engine, not_carried
|
|
289
|
+
|
|
290
|
+
|
|
220
291
|
def _schedule_type(sched: dict) -> str:
|
|
221
292
|
raw = (sched.get("schedule_type") or "static_prompt").strip()
|
|
222
293
|
resolved = _SCHEDULE_TYPE_ALIASES.get(raw)
|
|
@@ -676,6 +747,7 @@ def run_agent_import(
|
|
|
676
747
|
description = manifest.get("description")
|
|
677
748
|
credentials = _manifest_list(manifest, "credentials")
|
|
678
749
|
schedules = _manifest_list(manifest, "schedules")
|
|
750
|
+
engine, not_carried = read_runtime(manifest)
|
|
679
751
|
contract = load_export_contract(source)
|
|
680
752
|
known_agent_id, known_from = resolve_known_agent_id(
|
|
681
753
|
source, manifest, account_cfg.platform_url
|
|
@@ -707,6 +779,12 @@ def run_agent_import(
|
|
|
707
779
|
)
|
|
708
780
|
_line(f" Credentials: {len(credentials)} draft(s)")
|
|
709
781
|
_line(f" Schedules: {len(schedules)}")
|
|
782
|
+
# Without a known id, --update decides create-or-reattach only when it
|
|
783
|
+
# looks the agent up by name, so the plan cannot say which yet.
|
|
784
|
+
existing = True if known_agent_id else (None if update else False)
|
|
785
|
+
_line(f" Engine: {_engine_line(engine, existing)}")
|
|
786
|
+
if not_carried:
|
|
787
|
+
_line(f" Not carried to the cloud: {', '.join(not_carried)}")
|
|
710
788
|
|
|
711
789
|
if dry_run:
|
|
712
790
|
console.console.print()
|
|
@@ -779,6 +857,7 @@ def run_agent_import(
|
|
|
779
857
|
known_agent_id=known_agent_id,
|
|
780
858
|
update=update,
|
|
781
859
|
user_workspace_id=user_workspace_id,
|
|
860
|
+
engine=engine,
|
|
782
861
|
)
|
|
783
862
|
agent_id = agent["id"]
|
|
784
863
|
|
|
@@ -890,11 +969,17 @@ def _resolve_or_create_agent(
|
|
|
890
969
|
known_agent_id: str | None,
|
|
891
970
|
update: bool,
|
|
892
971
|
user_workspace_id: str | None,
|
|
972
|
+
engine: str | None = None,
|
|
893
973
|
) -> dict:
|
|
894
974
|
"""Return the cloud agent record, creating it when this is a first import.
|
|
895
975
|
|
|
896
976
|
Resolution is by id whenever the manifest carries one — a duplicate display
|
|
897
977
|
name on the platform must never make the import write into the wrong agent.
|
|
978
|
+
|
|
979
|
+
``engine`` reaches the platform only on creation. An existing agent keeps
|
|
980
|
+
its engine: the id-resolved case already said so in the plan. For --update
|
|
981
|
+
without an id the plan could not know, so the outcome is said here —
|
|
982
|
+
kept on a name-matched resume, sent when no agent matched.
|
|
898
983
|
"""
|
|
899
984
|
if known_agent_id:
|
|
900
985
|
items = client.list_account_agents().get("data", [])
|
|
@@ -925,6 +1010,8 @@ def _resolve_or_create_agent(
|
|
|
925
1010
|
f" Reusing existing agent {matches[0].get('name')} "
|
|
926
1011
|
f"({matches[0].get('id')})."
|
|
927
1012
|
)
|
|
1013
|
+
if engine:
|
|
1014
|
+
_line(f" Engine: {_engine_line(engine, existing=True)}")
|
|
928
1015
|
return matches[0]
|
|
929
1016
|
if len(matches) > 1:
|
|
930
1017
|
rows = ", ".join(f"{a['name']} ({a['id']})" for a in matches)
|
|
@@ -934,12 +1021,35 @@ def _resolve_or_create_agent(
|
|
|
934
1021
|
)
|
|
935
1022
|
|
|
936
1023
|
agent = client.create_agent(
|
|
937
|
-
agent_name, description, user_workspace_id=user_workspace_id
|
|
1024
|
+
agent_name, description, user_workspace_id=user_workspace_id, engine=engine
|
|
938
1025
|
)
|
|
939
1026
|
_line(f" Created agent {agent.get('name')} ({agent.get('id')}).")
|
|
1027
|
+
if update and engine:
|
|
1028
|
+
_line(f" Engine: {_engine_line(engine, existing=False)}")
|
|
940
1029
|
return agent
|
|
941
1030
|
|
|
942
1031
|
|
|
1032
|
+
def _engine_line(engine: str | None, existing: bool | None) -> str:
|
|
1033
|
+
"""The ``Engine:`` line's value — what the cloud will do with the preference.
|
|
1034
|
+
|
|
1035
|
+
``existing`` is ``None`` when that is not known yet: --update without a
|
|
1036
|
+
known id reattaches to an agent matched by name, or creates one.
|
|
1037
|
+
"""
|
|
1038
|
+
if engine is None:
|
|
1039
|
+
return "not set — the cloud agent gets the platform's default"
|
|
1040
|
+
if existing is None:
|
|
1041
|
+
return (
|
|
1042
|
+
f"{engine} — sent only if this import creates the agent; "
|
|
1043
|
+
f"--update keeps the engine of an existing agent it finds by name"
|
|
1044
|
+
)
|
|
1045
|
+
if existing:
|
|
1046
|
+
return (
|
|
1047
|
+
f"{engine} — not applied: --update never changes an existing "
|
|
1048
|
+
f"agent's engine"
|
|
1049
|
+
)
|
|
1050
|
+
return f"{engine} — sent as a preference (the cloud runs engines it does not support on OpenCode)"
|
|
1051
|
+
|
|
1052
|
+
|
|
943
1053
|
def _write_prompts_and_metadata(
|
|
944
1054
|
client: AccountClient, agent_id: str, source: Path, manifest: dict
|
|
945
1055
|
) -> None:
|
|
@@ -469,3 +469,18 @@ def test_a_bare_coded_body_is_still_a_coded_refusal(account_client):
|
|
|
469
469
|
account_client.install_skill_on_agent("agent-123", "pkg-uuid")
|
|
470
470
|
assert exc_info.value.code == "already_installed"
|
|
471
471
|
assert "already added" in exc_info.value.detail
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@respx.mock
|
|
475
|
+
@pytest.mark.parametrize("engine, sent", [(None, False), ("claude", True)])
|
|
476
|
+
def test_create_agent_sends_the_engine_only_when_set(account_client, engine, sent):
|
|
477
|
+
"""An older core ignores an unknown body field; a newer one maps it."""
|
|
478
|
+
route = respx.post("https://platform.example.com/api/v1/cli/account/agents").respond(
|
|
479
|
+
200, json={"id": "agent-1", "name": "Invoice Watcher"}
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
account_client.create_agent("Invoice Watcher", engine=engine)
|
|
483
|
+
body = json.loads(route.calls.last.request.content)
|
|
484
|
+
assert ("engine" in body) is sent
|
|
485
|
+
if sent:
|
|
486
|
+
assert body["engine"] == "claude"
|