diff --git a/.agents/README.md b/.agents/README.md index 87838916..0cb9c091 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -1,5 +1,15 @@ # `.agents/` — engineering record of the python-sdk repository +- [Agents API delivery operations](notes/proposed/process/2026-11-12-agents-api-delivery-operations.md) · [record-placement verification](evidence/verify/2026-11-12-agents-api-delivery-operations.md). + +- [Standalone server policy documentation](notes/proposed/feature/2026-11-12-standalone-policy-docs.md) · [verification](evidence/verify/2026-11-12-standalone-policy-docs.md). + +- [Shared YAML Core reader](notes/proposed/feature/2026-11-12-template-yaml-core-reader.md) · [verification](evidence/verify/2026-11-12-template-yaml-core-reader.md). + +- [Managed Session response projection](notes/proposed/feature/2026-11-12-managed-session-projection.md) · [verification](evidence/verify/2026-11-12-managed-session-projection.md). + +- [Canonical v1 runtime consumers](notes/proposed/feature/2026-11-12-template-v1-runtime.md) · [first freeze](evidence/verify/2026-11-12-template-v1-runtime.md) · [ports and process-domain comparison](evidence/verify/2026-11-12-template-v1-runtime-ports.md) · [final local gates](evidence/verify/2026-11-12-template-v1-runtime-final-local.md). + - [Managed Agent input and saved-configuration contract](notes/proposed/feature/2026-11-12-managed-agent-input-contract.md) · [verification](evidence/verify/2026-11-12-managed-agent-input-contract.md). ## Recent changes — 2026-11-10 diff --git a/.agents/evidence/verify/2026-11-12-agents-api-delivery-operations.md b/.agents/evidence/verify/2026-11-12-agents-api-delivery-operations.md new file mode 100644 index 00000000..eb672b20 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-delivery-operations.md @@ -0,0 +1,34 @@ +# Verification: python-sdk Agents API record placement + +Date: 2026-11-12. Source before this record-only follow-up: c4d43a24f40412bb8ab303509a7cb5bf7030da3c. +Result: PASS for local record-placement checks only. The overall cloud/topic delivery +remains incomplete. No product/runtime/post/CI/configuration changes are part of this follow-up. + +## Scope + +Add the owner operations note, index and any explicitly retained small local probe sources. +Necessary continuation instructions no longer require a workspace-only plan. Previously +committed evidence and original captures remain history; no old result is rewritten as a new run. +Cross-owner requirements use absolute GitHub links. Credentials, raw sensitive logs, installed +dependencies, caches and distribution binaries are excluded. + +## Verification + +- PASS: only owner .agents paths changed; 5 Markdown, 6 JSON and 1 retained Python sources checked. +- PASS: local links remain within this standalone repository, all seven note sections are in order, and retained source SHA256 values match exact originals. +- PASS: new record contents contain no configured private credential/endpoint values or workstation-specific source paths. +- Final `bash scripts/check-repo-health.sh --quiet`:106 PASS /1 existing recommendation /0 FAIL. Initial run105 PASS /1 WARN /1 FAIL reported the new dated probe-directory reference as a missing evidence slug; a conventional adjacent dated Markdown entry now owns that directory. No health gate was changed. +- PASS: staged whitespace and configured-private-value checks. CLI/SDK existing hygiene gates also pass; normal commit hooks run below. These outcomes remain independent of the earlier product CI checkpoint. + +Copying an original script is archival work, not a repeated model/cloud test. Retained probe +sources were syntax-checked without importing/executing them; no paid model or cloud operation +was performed for this follow-up. The audit environment used Python3.13.13 with the owner's +pinned record-validation dependencies. New exact-head remote CI remains a separate gate. + +## Remaining delivery + +Record-only local verification does not complete Managed FC deploy/update/old-Session/cleanup, +Codex tool-round acceptance, source publication or the two topics. Follow the +[owner operations](../../notes/proposed/process/2026-11-12-agents-api-delivery-operations.md) and its links for prerequisites and normal delivery gates. + +The normal secret hook initially rejected the archived constant synthetic loopback token. An explicit test-fixture comment resolves that classification under the existing rule; Python AST equivalence and original/retained SHA256 values are recorded. No hook/allowlist configuration was weakened and no model/cloud test was rerun. diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability.md b/.agents/evidence/verify/2026-11-12-agents-api-interoperability.md new file mode 100644 index 00000000..edf8e94d --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability.md @@ -0,0 +1,25 @@ +# Retained official SDK interoperability probe + +The accompanying directory retains the original probe execution semantics, actual distribution identity and before/after +typed results. It was not rerun during record placement. sources.json identifies every retained +byte sequence. No wheel, model request/response, credential or raw stderr is committed. + +The [recorded alignment table](2026-11-12-agents-api-interoperability/alignment.md) keeps the prior capability and topology decisions +with the SDK owner instead of requiring a workspace-only report. + +Use a separate local test environment with the matching reviewed client source and its +managed-agents dependencies, plus the actual official openai3.49.0 distribution identified in +distribution.json. Run `python .agents/evidence/verify/2026-11-12-agents-api-interoperability/probe.py` from the repository root and save any new typed result in a +new dated evidence file. OpenAI is not a product dependency. The probe binds only loopback, +uses temporary SQLite and a synthetic local token, starts no Worker and calls no model/cloud. +Review the source before execution; its create/retrieve/list result is not full API coverage. + +The after-projection result records default decoding success and remaining strict +Agent/harness/environment validation differences. A permissive default decoder is not a full +compatibility contract. The [SDK owner operations](../../notes/proposed/process/2026-11-12-agents-api-delivery-operations.md) +describe actual product gates and limits. Existing source versions/test results remain scoped +to their own captures, not an unexecuted current-head full run. + +The retained model-protocol-preflight.json is a separate configured-endpoint synthetic check: Chat tool-result200 and Responses tool-result400 for the selected model. It is not a general claim about all providers/models, an actual Codex task, or a new probe during record placement. The template owner separately holds the real local Qwen task results. + +The initial normal commit hook rejected the historical constant loopback token as a hardcoded assignment. The retained probe adds only an explicit synthetic test-fixture comment; its Python AST is identical to the original. sources.json preserves both byte hashes and that adaptation. No scanner rule was changed, no real credential was involved, and the recorded SDK result was not rerun or relabeled. diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/alignment.md b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/alignment.md new file mode 100644 index 00000000..55690340 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/alignment.md @@ -0,0 +1,38 @@ +# Recorded Agents API alignment boundary + +This is the 2026-11-12 review snapshot for SDK source +c4d43a24f40412bb8ab303509a7cb5bf7030da3c and official Python SDK3.49.0. It archives the +earlier comparison, not a new official-API lookup, model run or cloud acceptance. +Beta references and later source must be checked again before extending claims. + +The project runs the complete Agent CLI/model/tool loop inside Sandbox. The official hosted +harness with a self-hosted exec-server connection is a different topology. A matching URL +path does not establish that topology or its behavior. + +| Surface | Recorded Managed implementation and boundary | +| --- | --- | +| Model/runtime/capability discovery | Explicit admitted Codex/Qwen mapping and project capability endpoint. Adapter classes do not admit other runtimes to HTTP. One ordinary deployment has one actual model binding. | +| Saved Agent and Session | Selected owner-scoped resource subset; create201 and supported initial text. Saved Agent plus inline supported overrides creates a Session snapshot without changing the saved object. The earlier mutual-exclusion hypothesis was rejected. | +| Typed input | String/user input_text subset and optional message discriminator. Unsupported image/audio/tool forms are rejected rather than implied. | +| Active input | Cannot steer the current native turn through this path; busy input is rejected. Explicit durable submissions enqueue a later turn and are a project extension. | +| Turns/items/events | Implemented persistent query/event paths and replay boundaries. Actual FC realtime streaming still needs a live streaming ingress check; a completed buffered response is insufficient. | +| Input files/artifacts | Fixed input snapshots and immutable per-turn output bytes with budgets and retention. These are project-managed extensions, distinct from official hosted files and from the older broad local server's in-memory files. | +| Cancel and uncertainty | Cancellation needs confirmed stop; uncertain native delivery remains reconcilable. Lease expiry or a database receipt alone does not prove remote side effects stopped or exactly-once execution. | +| Close/delete | Close releases compute while retaining published results; DELETE removes public access and has separate busy/cleanup rules. Do not describe retained close results as resources still available after DELETE. | +| Official SDK decoding | Actual loopback HTTP/SQLite create/retrieve/list decode in default mode. Strict response validation still fails on Agent/harness/environment fields. See retained typed results; no Worker/model was started. | +| Narrow response projection | Public integer timestamps and truthful empty required_actions/vault_ids; internal clock precision is retained. No invented remote_url, inline saved-Agent ID, service tier, reasoning or multi-agent state. | +| Other known differences | Metadata limits and future-turn configuration do not match the full official reference. Arbitrary external tools/MCP, subagents, automations, webhooks, vault and trace are not delivered by this Managed subset. | +| Broad local server | Separate persistence, input/lifecycle and saved-Agent override behavior remain. Managed fixes cannot be described as fixes to every server entrypoint. | + +Recorded primary references: + +- [Official architecture](https://developers.openai.com/api/docs/guides/agents-api/architecture). +- [Session semantics](https://developers.openai.com/api/docs/guides/agents-api/sessions). +- [Create Session reference](https://developers.openai.com/api/reference/resources/beta/subresources/agents/subresources/sessions/methods/create). +- [Files/environments](https://developers.openai.com/api/docs/guides/agents-api/environments/files). + +The model protocol prerequisite is separately retained in model-protocol-preflight.json: +the configured model's Chat tool-result probe returned200 and Responses tool-result400. +That is not a general conclusion about other accounts/services, and not a real Codex task. +New FC/Qwen/Codex acceptance, revision routing and cleanup still require the owner operations +and their separate dated evidence. No success article is authorized by this comparison alone. diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/distribution.json b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/distribution.json new file mode 100644 index 00000000..824a82a4 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/distribution.json @@ -0,0 +1,10 @@ +{ + "version": "3.49.0", + "wheels": [ + { + "filename": "openai-3.49.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/fe/45/de8fbd25a1ca0a46f9641604a7cb991123daa7f292c27625223d685d9bac/openai-3.49.0-py3-none-any.whl", + "sha256": "3977754792115027ba80ae16996ba3366478b5b228e1ab58e2660bd7ac8c3d78" + } + ] +} diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/model-protocol-preflight.json b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/model-protocol-preflight.json new file mode 100644 index 00000000..7e562ae0 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/model-protocol-preflight.json @@ -0,0 +1,17 @@ +{ + "scope": "Configured endpoint minimal synthetic tool-result protocol probes, not Agent/cloud business E2E", + "results": [ + { + "probe": "chat-tool-result", + "model": "qwen3-coder-flash", + "httpStatus": 200, + "hasOutput": true + }, + { + "probe": "responses-tool-result", + "model": "qwen3-coder-flash", + "httpStatus": 400, + "toolRoleRejected": true + } + ] +} diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/probe.py b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/probe.py new file mode 100644 index 00000000..19c06a77 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/probe.py @@ -0,0 +1,43 @@ +"""Read-only local official SDK boundary probe; no Worker or cloud/model invocation.""" +import hashlib, inspect, json, pathlib, tempfile, threading +import openai +from agent_sandbox.managed_agents import ManagedConfig, ManagedService, ManagedStore +from agent_sandbox.managed_agents.http import TokenAuthenticator, make_server + +out = {'sdkVersion': openai.__version__, 'sdkModule': str(pathlib.Path(openai.__file__).name), 'operations': [], 'workerStarted': False, 'cloudCalls': 0} +with tempfile.TemporaryDirectory(prefix='abx-sdk-interop-') as tmp: + config = ManagedConfig.from_dict({'revision':'local-probe','template':'fixture-only','models':{'fixture-model':'qwen-code'}}) + service = ManagedService(ManagedStore('sqlite:///'+tmp+'/state.db', local=True), config) + token = 'local-only-probe-token' # synthetic test-only loopback fixture + auth = TokenAuthenticator({'tokens':[{'sha256':hashlib.sha256(token.encode()).hexdigest(),'owner':'local-probe','scopes':['agents:read','agents:write']}]}) + server = make_server(service, auth, port=0) + thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start() + sid = None + try: + for strict in (False, True): + with openai.OpenAI(api_key=token,base_url=f'http://127.0.0.1:{server.server_port}/v1',max_retries=0,timeout=5,_strict_response_validation=strict) as client: + ops = [('create', lambda: client.beta.agents.sessions.create(environment={'type':'self_hosted'},agent={'model':'fixture-model'},input=[{'type':'message','role':'user','content':[{'type':'input_text','text':'typed local prompt'}]}]))] if sid is None or strict else [] + ops += [('retrieve',lambda:client.beta.agents.sessions.retrieve(sid)),('list',lambda:client.beta.agents.sessions.list(limit=10))] + for name, operation in ops: + row={'strict':strict,'operation':name} + try: + value=operation(); row['decodedClass']=type(value).__name__ + body=value.model_dump(mode='json'); row['decodedFields']=sorted(body) + if name=='create': + sid=value.id; row['status']=value.status + if name=='list':row['count']=len(value.data) + if name=='retrieve':row['sameId']=value.id==sid + except Exception as exc: + row['errorClass']=type(exc).__name__ + if isinstance(exc,openai.APIResponseValidationError): + row['statusCode']=exc.status_code + cause=exc.__cause__ + if hasattr(cause,'errors'): row['validationErrors']=[{'loc':list(e['loc']),'type':e['type']} for e in cause.errors()] + else: row['message']=str(exc) + out['operations'].append(row) + with service.store.transaction() as tx: + out['acceptedTypedPrompt']=any(r.get('_prompt')=='typed local prompt' for r in tx.list('submission','local-probe')) + out['sessionCount']=len(tx.list('session','local-probe')) + finally: + server.shutdown();server.server_close();thread.join(timeout=3) +print(json.dumps(out,indent=2)) diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-after-projection.json b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-after-projection.json new file mode 100644 index 00000000..63f48e22 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-after-projection.json @@ -0,0 +1,614 @@ +{ + "sdkVersion": "3.49.0", + "sdkModule": "__init__.py", + "operations": [ + { + "strict": false, + "operation": "create", + "decodedClass": "AgentSession", + "decodedFields": [ + "agent", + "created_at", + "environment", + "error", + "id", + "last_active_at", + "metadata", + "object", + "required_actions", + "review", + "spend_control", + "status", + "usage", + "vault_ids", + "x_agent_sandbox" + ], + "status": "in_progress" + }, + { + "strict": false, + "operation": "retrieve", + "decodedClass": "AgentSession", + "decodedFields": [ + "agent", + "created_at", + "environment", + "error", + "id", + "last_active_at", + "metadata", + "object", + "required_actions", + "review", + "spend_control", + "status", + "usage", + "vault_ids", + "x_agent_sandbox" + ], + "sameId": true + }, + { + "strict": false, + "operation": "list", + "decodedClass": "SyncCursorPage[AgentSession]", + "decodedFields": [ + "data", + "first_id", + "has_more", + "last_id", + "object", + "x_agent_sandbox" + ], + "count": 1 + }, + { + "strict": true, + "operation": "create", + "errorClass": "APIResponseValidationError", + "statusCode": 201, + "validationErrors": [ + { + "loc": [ + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + } + ] + }, + { + "strict": true, + "operation": "retrieve", + "errorClass": "APIResponseValidationError", + "statusCode": 200, + "validationErrors": [ + { + "loc": [ + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + } + ] + }, + { + "strict": true, + "operation": "list", + "errorClass": "APIResponseValidationError", + "statusCode": 200, + "validationErrors": [ + { + "loc": [ + "data", + 0, + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + } + ] + } + ], + "workerStarted": false, + "cloudCalls": 0, + "acceptedTypedPrompt": true, + "sessionCount": 2 +} diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-before-projection.json b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-before-projection.json new file mode 100644 index 00000000..58bb37ba --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/result-before-projection.json @@ -0,0 +1,726 @@ +{ + "sdkVersion": "3.49.0", + "sdkModule": "__init__.py", + "operations": [ + { + "strict": false, + "operation": "create", + "decodedClass": "AgentSession", + "decodedFields": [ + "agent", + "created_at", + "environment", + "error", + "id", + "last_active_at", + "metadata", + "object", + "required_actions", + "review", + "spend_control", + "status", + "usage", + "vault_ids", + "x_agent_sandbox" + ], + "status": "in_progress" + }, + { + "strict": false, + "operation": "retrieve", + "decodedClass": "AgentSession", + "decodedFields": [ + "agent", + "created_at", + "environment", + "error", + "id", + "last_active_at", + "metadata", + "object", + "required_actions", + "review", + "spend_control", + "status", + "usage", + "vault_ids", + "x_agent_sandbox" + ], + "sameId": true + }, + { + "strict": false, + "operation": "list", + "decodedClass": "SyncCursorPage[AgentSession]", + "decodedFields": [ + "data", + "first_id", + "has_more", + "last_id", + "object", + "x_agent_sandbox" + ], + "count": 1 + }, + { + "strict": true, + "operation": "create", + "errorClass": "APIResponseValidationError", + "statusCode": 201, + "validationErrors": [ + { + "loc": [ + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "created_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + }, + { + "loc": [ + "last_active_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "required_actions" + ], + "type": "missing" + }, + { + "loc": [ + "vault_ids" + ], + "type": "missing" + } + ] + }, + { + "strict": true, + "operation": "retrieve", + "errorClass": "APIResponseValidationError", + "statusCode": 200, + "validationErrors": [ + { + "loc": [ + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "created_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + }, + { + "loc": [ + "last_active_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "required_actions" + ], + "type": "missing" + }, + { + "loc": [ + "vault_ids" + ], + "type": "missing" + } + ] + }, + { + "strict": true, + "operation": "list", + "errorClass": "APIResponseValidationError", + "statusCode": 200, + "validationErrors": [ + { + "loc": [ + "data", + 0, + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "created_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "last_active_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "data", + 0, + "required_actions" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 0, + "vault_ids" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "id" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "multi_agent" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "reasoning" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "service_tier" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "agent", + "text" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "created_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceNone", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "files" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "network" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "packages" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "plugins" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "skills" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceOpenAIHosted", + "type" + ], + "type": "literal_error" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceSelfHosted", + "capability_directories" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "environment", + "EnvironmentResourceSelfHosted", + "remote_url" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "last_active_at" + ], + "type": "int_from_float" + }, + { + "loc": [ + "data", + 1, + "required_actions" + ], + "type": "missing" + }, + { + "loc": [ + "data", + 1, + "vault_ids" + ], + "type": "missing" + } + ] + } + ], + "workerStarted": false, + "cloudCalls": 0, + "acceptedTypedPrompt": true, + "sessionCount": 2 +} diff --git a/.agents/evidence/verify/2026-11-12-agents-api-interoperability/sources.json b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/sources.json new file mode 100644 index 00000000..bd6b3245 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-interoperability/sources.json @@ -0,0 +1,28 @@ +{ + "files": { + "probe.py": { + "sha256": "355f301a088e13db7c36dde5cd4bcd77128ec87af5ad70a4e46c19e448306c42", + "original_sha256": "9df146330e47be60a5132bbdf48113eade47e14333cde0585f4253d3dc3c799f", + "source_scope": "Original probe plus synthetic-fixture classification comment only; execution AST is identical", + "ast_equivalent_to_original": true + }, + "distribution.json": { + "sha256": "b68165e448a5c30ca4fe63866790938cab27a148e6a20ce42655545e6d4decf6", + "source_scope": "retained original 2026-11-12 execution; copied byte-for-byte" + }, + "result-before-projection.json": { + "sha256": "df364a24517218b952f6fd265a9551a24023671a0045d3c3ddeea8c3ee05de92", + "source_scope": "retained original 2026-11-12 execution; copied byte-for-byte" + }, + "result-after-projection.json": { + "sha256": "26807ab800239c58d05e30ffe7ea3c942c5a347c9da5a2aa3f31c4298942f6dd", + "source_scope": "retained original 2026-11-12 execution; copied byte-for-byte" + }, + "model-protocol-preflight.json": { + "sha256": "6d5c957105adb299ee8d7102e7002f6d912b4ac70b54f13fe91a777c06b34030", + "source_scope": "retained original configured-endpoint probe; not a new API/model execution" + } + }, + "copied_at_utc": "2026-11-12T20:36:42.892576+00:00", + "reexecuted_during_record_placement": false +} diff --git a/.agents/evidence/verify/2026-11-12-agents-api-pr-checkpoint.json b/.agents/evidence/verify/2026-11-12-agents-api-pr-checkpoint.json new file mode 100644 index 00000000..118680ec --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-agents-api-pr-checkpoint.json @@ -0,0 +1,160 @@ +{ + "scope": "remote source checkpoint before record-only follow-up; new head checks remain separate", + "observed_at_utc": "2026-11-12T20:36:43.000655+00:00", + "pull_requests": [ + { + "repository": "python-sdk", + "number": 163, + "url": "https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163", + "head": "ef61cfebf009ba5ea6783a0a94735cc26e4d0b9b", + "draft": true, + "state": "open", + "merged": false, + "checks_read_blocked": false, + "checks": [ + { + "name": "Build packages", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Public runtime fixture (python-hello)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.12)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Python to Node provider engine", + "status": "completed", + "conclusion": "success" + }, + { + "name": "No local paths or secrets", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Pinned E2B public contract", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.10)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.12)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Public runtime fixture (agents-cli)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.11)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.11)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.10)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Markdown links", + "status": "completed", + "conclusion": "success" + } + ] + }, + { + "repository": "python-sdk", + "number": 164, + "url": "https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/164", + "head": "c4d43a24f40412bb8ab303509a7cb5bf7030da3c", + "draft": true, + "state": "open", + "merged": false, + "checks_read_blocked": false, + "checks": [ + { + "name": "Build packages", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.11)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.10)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.10)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Public runtime fixture (agents-cli)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.11)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Pinned E2B public contract", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Python to Node provider engine", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Public runtime fixture (python-hello)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "client-sdk (Python 3.12)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "server-sdk (Python 3.12)", + "status": "completed", + "conclusion": "success" + }, + { + "name": "No local paths or secrets", + "status": "completed", + "conclusion": "success" + }, + { + "name": "Markdown links", + "status": "completed", + "conclusion": "success" + } + ] + } + ] +} diff --git a/.agents/evidence/verify/2026-11-12-managed-session-projection.md b/.agents/evidence/verify/2026-11-12-managed-session-projection.md new file mode 100644 index 00000000..bbfc0f04 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-managed-session-projection.md @@ -0,0 +1,46 @@ +# Verification: managed Session public projection + +Date: 2026-11-12. Status: PARTIAL; local projection checks below do not replace final-head remote +CI, final Server image reconstruction or required cloud acceptance. Extends committed source +`c550cf53a9eb0c3c9899d0336b924633eb9b0988`; no package release or cloud/model invocation. +Part of [delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309), +not a claim that [input PR163](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163) +provides complete official SDK compatibility. + +## Regression and actual behavior + +Two new real HTTP regressions use temporary SQLite/PostgreSQL: before the fix both fail because +Session timestamps are fractional floats; after the fix both pass. Creation, retrieval and list +return integer seconds and empty action/Vault arrays. Observation preserves exact fractional +stored timestamps and a private deadline. Unsupported action/Vault request fields remain400; +no Sandbox is created. No official executor URL or effective model settings are fabricated. + +The targeted saved-Agent/typed-input/projection group passes6 tests. The independent official +SDK3.49.0 probe uses the actual SDK distribution, a real authenticated loopback Managed HTTP +listener and SQLite. Default create/retrieve/list still decode; all three strict methods still +raise `APIResponseValidationError` on remaining agent/environment fields. Fractional timestamp +and absent-list errors disappear. The actual accepted typed prompt is checked in the store. +The isolated probe adds no project dependency and starts no Worker/model/cloud connection. + +## Gate scope and failures retained + +The initial Managed group runner used an incorrect relative path because its capture helper +fixes repository cwd; two invocations returned4 with no tests run. Corrected paths are recorded +separately. The initial static run found formatting of a pending, untracked reader regression; +that file was formatted and is not part of this scoped API commit. These harness failures are +not converted to product passes. + +The earlier full local suite and CI remain bound to product1079841 / producer5cf448, as recorded +in [final local gates](2026-11-12-template-v1-runtime-final-local.md). They are not relabeled as +verification of this increment. The previously built c550 Server image is likewise historical; +this API change requires a new final Server source pin/build. The Guest image is unaffected. + +The namespace-isolated Managed runtime/entrypoint group passes95 tests. Repository lint, +typecheck (server58/client117 files), format-check and hygiene all exit0. Pytest emits the existing +unset asyncio fixture-loop-scope deprecation under the root capture helper. No test is suppressed +or weakened. Staged secret/diff checks and normal commit hooks are run for this scoped commit; +the optional pre-commit framework is absent as in the prior evidence. + +The initial normal commit hook rejected the new literal local-fixture token label as a potential +secret. Its explicitly test-only name was corrected to match existing fixture conventions; +no real credential was involved. The rejection and subsequent successful checks are retained. diff --git a/.agents/evidence/verify/2026-11-12-standalone-policy-docs.md b/.agents/evidence/verify/2026-11-12-standalone-policy-docs.md new file mode 100644 index 00000000..088359f2 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-standalone-policy-docs.md @@ -0,0 +1,35 @@ +# Verification: standalone policy documentation + +Date: 2026-11-12. Scope: documentation only, extending +`b988d2a76057b6202d836ec329cc0bd90a572b26`. Part of +[SDK PR164](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/164) and +[delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). + +The actual failed [Markdown job](https://github.com/Alibaba-Agent-Sandbox/python-sdk/actions/runs/43283073913/job/131409205320) +reports exactly two missing fragments in bilingual file-and-command-controls links to +`template-yaml-spec.md`. Their surrounding instructions also teach the removed root server +block. Independent adversarial review approved correcting all seven active bilingual topics. + +Changes preserve standalone JSON policy, YAML policy with PyYAML, Python policy APIs and actual +server loading/validation commands. Advanced policy is optional; normal resources retain one +manifest. The image example explicitly copies its guest policy file. Managed bridge route +configuration is not represented as SandboxServer policy. Existing archive/design/history is +unchanged. The related active ports prerequisite now refers to real Provider endpoint operations. + +Local verification: + +- Actual lychee0.24.2, same offline/include-fragments/exclude-agents arguments as remote CI: + 1502 links total,458 unique,1190 OK,312 excluded,0 errors; exit0. +- Real ServerPolicy:12 complete JSON/YAML examples plus2 Python policy objects validate/bind. + Actual `check-config --policy` runs for both languages; a separate real YAML policy file loads. +- All14 active pages checked:zero root server examples and removed reference fragments. +- Repository health/hygiene and staged secret/diff checks are recorded before scoped commit. + +The platform artifact mount refused an executable chmod for the downloaded checker; an identical +binary was executed from temporary tool storage. No product permission or global setting was +changed. No runtime tests or cloud/model matrix are repeated merely for prose. + +The final b988 Server image has separately passed actual image/local HTTP/SQLite/Core probes and +pipcheck; this documentation increment changes no image runtime bytes and needs no new recipe +pin/build. The older c550 image evidence remains historical. Final-head remote CI and the +required cloud acceptance gate are not claimed by these documentation checks. diff --git a/.agents/evidence/verify/2026-11-12-template-v1-runtime-final-local.md b/.agents/evidence/verify/2026-11-12-template-v1-runtime-final-local.md new file mode 100644 index 00000000..f5ebd7cb --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-template-v1-runtime-final-local.md @@ -0,0 +1,133 @@ +# Verification: final local v1 runtime consumer gates + +- Date: 2026-11-12 +- Product source: `1079841307f1f63253ae68fdda6e6d2d1e77e851`, extending + `ab50ad8e750b15221c25ccf231caffdc3f196983`, stacked on + [API PR163](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163). +- Full-suite producer source: [5cf4482](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/5cf448296361b3bcd8341bd8ae8131f2ca703496). +- Final contract source: [fad843f](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/fad843f60881b405cb386537337817e5144892bc). +- Result: PARTIAL — reported local checks pass within their exact source scopes; + actual final-head remote CI and required cloud acceptance remain pending. +- Packages/environment: matching source client/server0.4.0a7, Python3.11.15, Linux. + Actual `jsonschema`4.26.0, hatchling1.33.0, psycopg3.3.2 and pgserver0.1.4 were installed. + No package release was created. +- Tracker: [templates309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). + +## What was verified + +Canonical v1 literals and resolved snapshots reach prepared creation with schema-validated +instance overrides. Actual Express request serialization forwards specs, TTL, env, metadata +and borrowed Volume names. Managed Session snapshots use one TTL source and contain safe +instance values plus environment names; Worker private config supplies custom values without +copying them into settings, plans or ownership receipts. Custom synchronous Provider schemas +are actually invoked, while absent/default/async validators fail before side effects. +Additional ports resolve actual Provider descriptors and gateway headers without inventing +service clients, readiness or native TCP access. Failure retains owned cleanup behavior. + +## Isolation and preserved failure evidence + +The earlier nonisolated full-suite runs remain failures. An unchanged baseline and the +candidate both reject compute reuse when an unrelated, same-UID unscoped process appears +inside the observed process domain. Both pass when that controlled process is absent. +The controlled child processes were terminated and waited for. This demonstrates a local +shared-host interference condition but does not retrospectively prove the original full +failure's cause, because its scan counters were not captured. + +For final full runs, `unshare -Upf --map-current-user --mount-proc` provided a real private +PID/proc namespace while keeping the current non-root UID, allowing actual PostgreSQL. +No process scanner, native receipt, assertion, grace period or cancellation state machine +was patched for the accepted full runs. This environment correction affects only the +validation subprocess and leaves the shared host namespace unchanged. + +Correction to earlier summaries in this workstream: both captured v1 failed full runs had +**Server880 passed, zero skips**, not875 passed/1 skip. The older count was copied from an +earlier environment before packaging prerequisites were present. Actual captured Client +outcomes remain2566 passed/26 skipped/8 stale fixture failures, then2586 passed/20 skipped/ +1 active-cancel reuse failure. Those histories are retained rather than rewritten as passes. + +## Commands and results + +| Command | Result | Scope | +| --- | --- | --- | +| Prefreeze focused v1/discovery/catalog/provider-boundary regressions | PASS230 | Canonical source e925051 before ports increment | +| Ports plus existing launch/Sandbox checks | PASS189 | Canonical source5cf448; actual HTTP serializer and endpoint audience | +| Namespace active-cancel + SQLite/PostgreSQL snapshot probe | PASS3 | Actual local components, no cloud | +| `unshare -Upf --map-current-user --mount-proc make test` | PASS | Server880 passed; Client2596 passed,20 skipped | +| `make lint typecheck format-check hygiene` | PASS | Both packages, source formatting/typing/hygiene | +| `bash scripts/check-repo-health.sh` | PASS |106 passed, one existing recommended English release-guide warning, zero failures | +| Source sync `--check` against actual producer Git objects | PASS | Exact normalizer/contract/schema/fixture bytes, immutable source lock | + +`unshare -Upf --map-current-user --mount-proc make ci`: **PASS**, exit0 in500.49s. +Server880 passed; Client2586 passed,20 skipped,10 integration cases deselected. Both +sdist/wheels built, all twine checks passed, both `py.typed` markers were present, and a +fresh client-only install performed actual local HTTP plus literal v1 normalization. +The optional local server extra installed and registered commands, and `pip check` passed. +These are local build/install results, not a package publication. + +CI retained one `PytestUnraisableExceptionWarning` from the existing +`test_reused_candidate_keeps_real_http_and_pty_listeners_separate[True]`: a websockets +handshake coroutine was finalized after its event loop closed. The test itself passed; +the full `make test` run did not report this warning. No handler or warning filter was +changed to hide it. A targeted retry is recorded separately below; it cannot erase +what the complete CI run actually reported. + +The warning case's targeted retry passed1 test without reproducing the unraisable warning; +the complete CI's original warning remains recorded. That retry does not establish a root cause. + +After the final producer commit removed the engineering `_skeleton` public-name exception +and tightened shared admission fixtures, the SDK copied its exact normalizer/contract/schema/ +fixture bytes and updated the source lock. The final source is **fad843f**, not the5cf448 +source from the complete full-suite run above. The final delta suite (owner admission, +launch, discovery, models, offline catalog, application binding/auth) passed **306 tests**. +Final `make lint typecheck format-check hygiene` and actual producer Git-byte comparison +both returned0. No old full-suite result is relabeled as an unexecuted final-head full run; +the actual final head still requires remote CI. + +Final staged secret, diff, source-lock and documentation/record checks are recorded below. +The single-interpreter check does not replace the remote Python3.10/3.11/3.12 matrix. + +The20 existing Client skips are explicit local-engine prerequisites:2 tests require an +`ABX_COMMAND_TEST_BIN`,5 need the legacy sibling CLI fixture path, and13 engine integration +cases need that CLI/dependency layout (Container4, Cube3, E2B6). The new v1/ports/private-env/ +PostgreSQL tests are not skipped. These skips are not missing cloud credentials. + +## Documentation and source recipe boundary + +Bilingual resource, authoring, launch and API references describe v1 and the new endpoint +method. Parsing prepared literals does not install a CLI; selected Providers may still have +runtime dependencies. The Managed Server must install a matching immutable client source +commit to consume instance/env fields; unchanged version metadata does not turn an older +published0.4.0a7 artifact into this source. Guest bridge/server production code and its +protocol remain unchanged. The template owner verifies that source recipe separately. + +## Cleanup and remaining gates + +No cloud resources or model calls were created by these checks. Local PostgreSQL fixtures +and controlled probe children were cleaned up. A read-only baseline worktree was used for +comparison; it created no deployment. Staged sources contain no credentials or private logs. + +Real cloud/model/FC deployment, actual arbitrary port reachability, remote matrix, official +OpenAI SDK interoperability, package publication and merged delivery are not established. +The local server's saved-Agent plus inline override rejection remains a separately identified +compatibility difference; this patch does not claim to fix every server entrypoint. + +Related history: [first freeze](2026-11-12-template-v1-runtime.md), +[ports and cancel comparison](2026-11-12-template-v1-runtime-ports.md), +[decision](../../notes/proposed/feature/2026-11-12-template-v1-runtime.md). + +## Final staged review + +Only bilingual API/launch guidance, the decision/index links and this independent evidence +were added after the consumer-code freeze. The only runtime-source changes after1079841 +are the exact final canonical owner files/source lock described above; no SDK launch, +Provider, Managed or cancellation implementation was modified. Historical +failed records and cancelled-process controls remain available as evidence. Staged secret +scan, `git diff --cached --check`, current repository health/hygiene and actual producer-byte +sync checks are required before the final evidence commit; their actual outcomes follow. + +Final outcomes: staged custom secret scan0; staged diff check0; producer source-byte sync0; +health106 PASS /1 existing recommendation /0 FAIL; hygiene0. The repository hook also +runs the custom secret scanner at commit. Its optional pre-commit framework is not installed +in this validation environment; the hook reports that fact rather than bypassing a configured +framework run. Full local CI above separately performed the mandated lint/format/type/test/ +build/install checks. Credentials remained in ignored sources and were not staged. diff --git a/.agents/evidence/verify/2026-11-12-template-v1-runtime-ports.md b/.agents/evidence/verify/2026-11-12-template-v1-runtime-ports.md new file mode 100644 index 00000000..8f127f34 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-template-v1-runtime-ports.md @@ -0,0 +1,64 @@ +# Verification: additional v1 ports and final runtime gates + +- Date: 2026-11-12 +- Base: consumer freeze `ab50ad8e750b15221c25ccf231caffdc3f196983`. +- Producer: [5cf4482](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/5cf448296361b3bcd8341bd8ae8131f2ca703496). +- Result: PARTIAL. Ports component checks pass; full gates and cloud acceptance remain open. +- Tracker: [templates309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). + +## Scope and verified behavior + +Additional ports remain declarations, separate from protocol-bearing services. Discovery +and canonical compilation preserve their sorted union with service ports. The prepared +launcher requires `ports.resolve` for declared ports, resolves extra descriptors using the +existing Provider identity/audience/expiry checks, and never constructs a service client or +sends readiness probes for these ports. Descriptor headers stay in memory; plans/journals +contain no gateway token. Port declarations are not native TCP/public access guarantees. +Managed public Gateway/private bridge ports are unchanged. + +`python -m pytest client-sdk/tests/test_template_v1_runtime.py +client-sdk/tests/test_api/test_template_launch.py client-sdk/tests/test_api/test_sandbox.py +-q --tb=short`: **189 passed**. This exercised actual Express HTTP serialization with +HTTP mocks, descriptor traffic-token audience, no extra requests/client construction, +provider mismatch cleanup, port union/defaults and existing application behavior. +Native cloud ports were not contacted. Producer source bytes are copied exactly, with +source-lock verification against the actual Git objects. + +## Full-suite failure retained + +The final `make test` attempt before this ports freeze returned exit2: Server875 passed / +1 skipped; Client2586 passed /20 skipped /1 failed. The existing active-cancel reuse +assertion failed again because its local backend resource had been disposed. This is a +required gate failure; it is not silently converted to a pass or dismissed by a retry. + +No Worker cancellation, guest bridge or cancellation-test production behavior changed. +A separate local probe ran the original test with both the unchanged API-contract baseline +`ef61cfebf009ba5ea6783a0a94735cc26e4d0b9b` and the candidate source: + +| Variant | Observed process-domain receipt | Original assertion | +| --- | --- | --- | +| Baseline, no controlled new process | `unknown_new_processes=0`, `stop_confirmed=true` | PASS | +| Candidate, no controlled new process | `unknown_new_processes=0`, `stop_confirmed=true` | PASS | +| Baseline, controlled unrelated same-UID child after baseline | `unknown_new_processes=1`, guest `stop_confirmed=false` | FAIL, same resource-reuse assertion | +| Candidate, same controlled child | `unknown_new_processes=1`, guest `stop_confirmed=false` | FAIL, same assertion | + +This establishes the existing conservative process-domain behavior on the shared host: +unattributed new processes forbid reuse, and the Worker confirms stopping by destroying +owned compute. It does not prove the reason in the original full-suite failure, which did +not capture process-domain scan counts. No process filters or assertions were weakened. +Controlled child processes were terminated and waited for; baseline worktree was read-only. +The no-side-effect `unshare -Urpf --mount-proc true` probe succeeded. A further +`unshare -Upf --map-current-user --mount-proc id -u` probe preserved the current UID, +which avoids PostgreSQL's root prohibition while isolating `/proc`. The next full run +will use that actual PID/user namespace; no process-observation code is patched. + +## Not verified + +Cloud/model/FC joint operation, actual arbitrary port reachability, remote Python matrix, +package release and merged delivery. Source metadata version remains0.4.0a7; a pinned +source recipe must not be described as a newly published alpha. This evidence extends, +rather than overwrites, the [earlier freeze record](2026-11-12-template-v1-runtime.md). + +`make lint typecheck format-check hygiene`: PASS. Repository health:106 PASS /1 existing +recommended release-guide warning /0 FAIL. Custom staged secret scan and diff checks run +before the source freeze. Full test/CI and cloud gates remain pending at this commit. diff --git a/.agents/evidence/verify/2026-11-12-template-v1-runtime.md b/.agents/evidence/verify/2026-11-12-template-v1-runtime.md new file mode 100644 index 00000000..08078f91 --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-template-v1-runtime.md @@ -0,0 +1,75 @@ +# Verification: canonical template v1 runtime consumers + +- Date: 2026-11-12 +- Change: `feat/template-v1-runtime-20261112`, stacked on SDK API-contract commit + `ef61cfebf009ba5ea6783a0a94735cc26e4d0b9b` / [PR163](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163). +- Result: PARTIAL — targeted local checks pass; final full-suite/CI and real-cloud gates pending. +- Package: matching source client/server0.4.0a7, Python3.11.15 on Linux; no release performed. +- Producer: [e925051](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/e9250512967d4c4abb59672567d68ba3e7d18473), + [PR310](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/pull/310). +- Tracker: [templates309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). + +## What was verified + +SDK-owned prepared launch/discovery consume v1, and Managed Sessions retain safe instance +snapshots while the Worker alone supplies custom env values. Wire tests use the actual +Express serializer and HTTP client with an HTTP mock; these are component regressions, +not live cloud. SQLite and temporary PostgreSQL are actual local database processes/files. +The tests retain existing startup/readiness and owned-cleanup assertions. + +## Commands run + +| Check | Result | Scope | +| --- | --- | --- | +| Selected launch/discovery/model/contract tests | PASS101 | Migrated old fixtures, canonical v1 admission | +| Additional registry/discovery/integration tests | PASS81 | Source loading and existing local SDK/server bridge | +| v1 runtime regressions + original Managed tests, first run | FAIL1 / PASS95 | Existing active-cancel test observed resource disposal where it expected reuse | +| That active-cancel test plus new runtime regressions, isolated retry | PASS27 | Does not erase the earlier timing observation | +| Final focused tests, including canonical fixtures and integration | PASS196 | Before full-suite capture; see subsequent gate results below | +| Prefreeze targeted v1/runtime/discovery/catalog/provider-boundary set | PASS230 | Canonical producer e925051; actual schema, plugin callback, HTTP serializer, SQLite/PostgreSQL | +| First `make test` | FAIL8 / Client2566pass26skip; Server875pass1skip | Eight old fixture catalog requirements for retired publication fields/specification5, corrected in fixture tests | +| Corrected fixture/runtime subset | PASS127 | No production schema relaxation; publication keys rejected in resource documents | +| `make lint typecheck format-check hygiene` | PASS | Both distributions, strict typing, formatting and source hygiene | +| `bash scripts/check-repo-health.sh` | PASS106 / WARN1 / FAIL0 | Existing recommended English release-guide warning | +| Final `make test` / `make ci` | PENDING | Frozen product head will be tested; separate evidence update follows | + +Intermediate schema-fixture runs exposed missing test Provider contexts and descriptor +callbacks. Those harness errors were corrected by supplying the actual fixture context and +real JSON Schema descriptor, without weakening production validators. First migration run +also contained three stale test expectations (a dropped invalid fixture and illegal +sandbox+deployment combination under the then-current producer schema); they were corrected. + +## Cancel timing observation + +The failing existing assertion was in +`test_active_cancel_exact_target_replays_without_cancelling_next_turn`: cancellation was +confirmed, one native instance had been created, but the local backend no longer retained +it. Neither `managed_agents/worker.py`, guest managed bridge code nor the cancel test was +changed. The existing Worker deliberately disposes owned compute when a terminal guest +receipt does not confirm stopping, then records confirmed cleanup. That is a possible +explanation, not an established root cause: the first run did not capture that private +receipt. The isolated retry passed. Full-suite outcomes below retain their independent +meaning; no assertion or timeout was weakened to obtain a pass. + +## Source freeze + +The parent requested an actual complete consumer commit so Templates/CLI/Registry can pin +the SDK-owned generic launch contract. This freeze does not imply merge, release, completed +full CI or cloud acceptance. The source includes the runtime migration, contract/vectors, +bilingual guidance and engineering records together; later validation gets a separate +evidence commit. Final source-lock verification compared actual producer Git object bytes. +The optional Provider callback uses real synchronous schemas; the default unavailable +method and asynchronous returns do not qualify as implementations. + +## Cleanup + +No cloud Sandbox, FC, database service, image, model operation or other cloud resource was +created. Temporary PostgreSQL instances call their actual fixture cleanup and use isolated +local directories. HTTP mock cleanup checks that only owned Sandbox DELETE occurs; no +Volume lifecycle call occurs. This does not establish live-account Volume behavior. + +## Not verified + +Real cloud/model/FC integration, remote Python matrix, package publication, merged delivery +and official OpenAI SDK interoperability are not established by these local checks. Required +cloud acceptance remains pending; the record and ADR stay proposed/PARTIAL. diff --git a/.agents/evidence/verify/2026-11-12-template-yaml-core-reader.md b/.agents/evidence/verify/2026-11-12-template-yaml-core-reader.md new file mode 100644 index 00000000..31e289eb --- /dev/null +++ b/.agents/evidence/verify/2026-11-12-template-yaml-core-reader.md @@ -0,0 +1,48 @@ +# Verification: shared YAML1.2 Core file admission + +Date: 2026-11-12. Result: PARTIAL — relevant local checks pass as detailed below; actual final-head +remote CI and required cloud acceptance remain separate. Extends Session projection source +`bd1db5215070917619dcce6593c48f432339df3f`. Reader producer: +[ef695803cb78bf82b8473195fcb37b3e6357a363](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/ef695803cb78bf82b8473195fcb37b3e6357a363). +Source lock and copied normalizer/schema/contract/object vectors/raw text vectors match exact +Git object bytes. Part of [delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). + +## Actual parser and file behavior + +Six independent before-regressions fail with the original PyYAML reader: plain on/date values +are changed at registry loading, cache discovery and prepared launch; duplicate keys, aliases +and UTF16 are accepted. The shared Core reader plus bounded byte I/O fixes all three public file +entries. A separate oversized-file regression confirms rejection before Sandbox/journal I/O. +A valid literal file actually forwards unchanged environment strings to the prepared launch +creation call. No CLI/parser expression evaluator or cloud connection is introduced. + +The packaged owner's13 positive and25 negative raw YAML text cases run through the installed +actual `yamlcore==0.1.1` loader. Valid cases also test byte input. Error strings omit source +canaries. An independent regression proves the shared Core loader does not globally change +PyYAML's existing YAML1.1 behavior for unrelated callers. + +## Local gates + +- Relevant raw-file, source-schema, instance, launch, discovery, registry/model and launch-contract + group:290 passed, no skips. Existing root capture emits the unset asyncio fixture-loop-scope + deprecation; no assertion is suppressed. +- Repository lint, typecheck (server58/client117 files), format-check and hygiene:exit0. +- Both sdists/wheels build, twine, py.typed, fresh client-only install/real HTTP, local server-extra + registration and pipcheck (`make build dist-check`):exit0. +- A second fresh client-wheel install outside the checkout verifies yamlcore0.1.1, all13/25 raw + text vectors, actual public file loading and every bundled source hash; pipcheck:exit0. +- Immutable producer byte comparison:exit0. + +The first health check ran while this evidence file had not yet been written and returned1 for +its two unresolved local record links. Its corrected result is recorded separately; the existing +recommended maintainer release-guide warning is not a new product failure. + +## Scope retained + +The earlier full local suite/CI remains tied to product1079841 / producer5cf448; see +[exact historical scope](2026-11-12-template-v1-runtime-final-local.md). It is not relabeled as +full-suite validation of this reader increment or the later Session projection. Current +final-head CI must run on the actual committed head. No package release, model task, cloud +resource creation, Guest reconstruction or new cloud acceptance matrix occurred. Managed Server +consumes compiled JSON; its final source pin/rebuild is needed for the preceding Session +projection change, while the c550 image evidence remains historical. diff --git a/.agents/notes/proposed/feature/2026-11-12-managed-session-projection.md b/.agents/notes/proposed/feature/2026-11-12-managed-session-projection.md new file mode 100644 index 00000000..34d445e7 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-11-12-managed-session-projection.md @@ -0,0 +1,63 @@ +# Decision: project truthful Session fields in the managed Agents API + +Status: proposed + +## Problem + +The actual official Python SDK3.49.0 accepts the managed Session routes with permissive decoding, +but strict validation rejects fractional Unix timestamps and absent empty action/vault lists, +alongside larger agent/environment topology differences. This is independently reproduced over +real local HTTP and SQLite, without a Worker, model, cloud resource or mocked HTTP response. + +## Decision + +Project Session `created_at` and `last_active_at` as integer Unix seconds. Return +`required_actions: []` and `vault_ids: []`: this managed implementation exposes no supported +client-required action or Vault binding. Preserve all internal timestamps, deadlines and durable +precision. Keep unsupported inputs explicitly rejected; do not manufacture official settings or +executor connection URLs merely to satisfy a client type. + +## API Design + +The common public Session projection applies to create/get/list/update/events without changing +the stored Session object. Other resource types keep their existing timestamp contract. +Unsupported `vault_ids` and `required_actions` request fields continue returning400. No new +required-action resource or Vault capability is introduced. Actual default official SDK +create/retrieve/list decode; strict validation still fails on unrelated agent/environment fields. + +## Alternatives considered + +- Change shared storage timestamps: rejected because internal lease/deadline ordering needs + its existing precision, and only the wire projection requires integer seconds. +- Fill every missing official field with defaults: rejected; the actual CLI/model effective + settings and OpenAI self-hosted executor topology do not follow from field names. +- Return null lists: rejected; current official reference/SDK requires arrays, and empty arrays + truthfully describe the unavailable action/Vault capability. + +## Dependencies + +Part of [delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309), +stacked after [narrow input PR163](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163). +Official [Create Session reference](https://developers.openai.com/api/reference/resources/beta/subresources/agents/subresources/sessions/methods/create) +returns timestamps as integer seconds and both fields as arrays. The actual SDK probe was isolated +from project dependencies and does not add an `openai` runtime dependency or create a release. + +## Test Strategy + +Use real authenticated loopback HTTP and both temporary SQLite/PostgreSQL. Assert create/get/list +projections, exact fractional values in the underlying stored object and no fabricated remote URL. +Pair empty response fields with rejected unsupported request fields. Re-run existing managed +runtime tests in a private PID/proc namespace, lint/type/format/hygiene and the actual official SDK +probe. Preserve earlier full CI scope; no local/cloud acceptance claims are extended. + +## Acceptance criteria + +- All public Session projections use integer seconds and truthful empty lists. +- Existing stored timestamp/deadline values remain unchanged by observation. +- Unsupported action/Vault requests fail before Sandbox creation. +- Default SDK decode success is distinguished from remaining strict-schema failure. +- Required local/remote/cloud gates and the need to rebuild a final Server image are explicit. + +## Evidence + +[Verification](../../../evidence/verify/2026-11-12-managed-session-projection.md). diff --git a/.agents/notes/proposed/feature/2026-11-12-standalone-policy-docs.md b/.agents/notes/proposed/feature/2026-11-12-standalone-policy-docs.md new file mode 100644 index 00000000..17c1f939 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-11-12-standalone-policy-docs.md @@ -0,0 +1,54 @@ +# Decision: document standalone SandboxServer policy loading + +Status: proposed + +## Problem + +Actual PR164 Markdown links CI on b988 failed on two removed server-policy anchors. Reviewing +the source context found fourteen active bilingual pages that still described a root `server` +resource block or automatic template-to-policy compilation, neither admitted by template v1. + +## Decision + +Correct seven active topics in both languages, retaining ordinary `template.yaml` as the single +resource entry. Present optional advanced SandboxServer restrictions as a standalone policy +with real loading commands. Preserve existing JSON/YAML policy support and Python APIs. + +## API Design + +No runtime API changes. JSON policy examples contain their actual policy keys without the +removed wrapper. `ABX_SERVER_POLICY`, `ABX_SERVER_POLICY_FILE`, `serve --policy`, `check-config` +and `ServerPolicy` retain their existing meanings. An authored image explicitly copies the +file into the guest path. Managed Agents API bridge/bootstrap controls are separate. + +## Alternatives considered + +Only fixing the two fragments leaves executable instructions that v1 rejects. Reintroducing +a root resource block or automatic compiler adds an unnecessary configuration contract. +Removing standalone YAML policy support contradicts the actual runtime and is not justified. +Historical/archive/design documents remain unchanged. + +## Dependencies + +[SDK PR164](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/164) and +[delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). +Actual existing `ServerPolicy.from_file` supports JSON always and YAML when PyYAML is installed. +No dependency, package release, runtime, Server recipe or Guest image change is required. + +## Test Strategy + +Run the same actual lychee0.24.2 offline/fragments command as the failed remote job. Validate +complete documented policy examples with real ServerPolicy, actual check-config in both +languages and a real standalone YAML file. Check active-page obsolete claims, relative links, +repository health/hygiene, scoped secret/diff checks and exact runtime-source immutability. + +## Acceptance criteria + +The fourteen pages agree on supported loading and fields. The existing explicit v1 anchor +resolves in both languages. Examples are actual standalone policies and correctly loaded guest +files. JSON/YAML/runtime API support remains accurate. Only documentation/records are changed; +new remote CI remains bound to its actual head, not inferred from the local result. + +## Evidence + +[Verification](../../../evidence/verify/2026-11-12-standalone-policy-docs.md). diff --git a/.agents/notes/proposed/feature/2026-11-12-template-v1-runtime.md b/.agents/notes/proposed/feature/2026-11-12-template-v1-runtime.md new file mode 100644 index 00000000..a12dfa46 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-11-12-template-v1-runtime.md @@ -0,0 +1,118 @@ +# Decision: Consume canonical v1 resources in prepared launch and Managed Sessions + +Status: proposed + +## Problem + +The template owner now defines public `specVersion: 1` resources, but Python launch, +discovery and registry loading still require flat specification5 fields. Managed Sessions +ignore instance CPU/memory/disk/GPU, existing Volumes and metadata. Their native Sandbox +TTL comes from a separate private budget. Persisting the resolved instance wholesale would +also copy custom environment values into durable Session state. + +## Decision + +Bundle immutable template-owner normalizer/schema bytes and their Git/hash lock. Validate +with actual JSON Schema; consume literals or a Devs-resolved v1 snapshot without installing +the CLI or interpreting a second expression language. Python does not prepare source or +partially execute Serving intent. Separate template preparation props from runtime instance +settings. Apply explicit API overrides before resolved validation and retain existing +startup/readiness, ownership journaling, uncertainty and cleanup behavior. + +Managed configuration snapshots only safe instance settings and custom environment names. +The Worker receives environment values from a distinct private FILE/JSON source, validates +exact names and injects them into the native Sandbox and bridge process. Protect system +credentials, model controls, ownership tags, the exclusive workspace and private runtime +state. Existing Volumes remain borrowed resources and are never deleted by Session cleanup. + +## API Design + +`launch_template` accepts public v1 documents. `timeout=None` preserves `instance.timeout` +(default300 for ordinary launch); explicit CPU/memory/disk/GPU/mount values replace their +instance fields, while explicit env/metadata maps merge by name. Instance overrides receive +resolved schema validation before journaling or creation. Unresolved `${...}` input fails; +an explicit literal API override may replace that value without evaluating it. + +Compiled `ManagedConfig` accepts `instance` without `envs`, and `instanceEnvNames`. +Managed TTL defaults3600; private `sessions.maxLifetimeSeconds` is derived and must agree +when both are explicitly supplied. Session `_settings` retains the safe instance snapshot +and names. Only Worker mode loads `ABX_MANAGED_SANDBOX_ENV_JSON` or `_FILE`; it never +forwards the host environment wholesale. `ExpressBackend(..., instance_env=...)` validates +the current Session's declared names before infrastructure I/O. + +Managed lifecycle is secure, kill-on-timeout, without auto resume. Conflicting/unsupported +fields fail. User tags cannot replace `abx-managed`, `abx-session`, `abx-revision`. Mounts +cannot overlap the workspace or `/home/user/.agent-sandbox`, including ancestors and +children. The current mount shape has no read-only flag; shared data mounts may be writable +and are not independent tenant storage boundaries. + +The optional synchronous `Provider.validate_template_input(stage, value)` seam binds actual +version-specific schemas to the selected Provider. The inherited unavailable default is not +counted as a validator. Missing unknown-plugin validators, false/raised rejection and async +returns fail before creation. This adds no operation claim or automatic vendor-field mapping. + +Additional `sandbox.ports` remain distinct from protocol-bearing `sandbox.services`. +The compiled/discovery port list is their sorted union. `TemplateSession.endpoints` +contains Provider descriptors only for additional ports not already represented by services. +`Sandbox.application_endpoint` factors the existing identity/audience/expiry checks from +`application_client`; no ServerClient, health call or startup is invented for raw ports. +Headers stay only in memory, and `ports.resolve` is required even without services. This +does not promise native TCP or public connectivity. + +Discovery validates canonical intent and derives service information without resolving +native expressions, credentials or cloud capabilities. The standalone server policy helper +remains available; root `server` is not silently added to the public resource schema. + +## Alternatives considered + +- Reimplement the v1 schema/defaults in Python: rejected; source ownership and shared + vectors require immutable producer bytes and an actual validator. +- Install or shell out to the full CLI for prepared literals: rejected; no source build or + expression execution is needed. Native expressions require the real Devs parser. +- Persist all resolved env values or put them in deployment plans: rejected; arbitrary + custom environment values are sensitive even without a secret-looking name. +- Ignore lifecycle, mounts, instance values or unsupported Serving declarations: rejected; + successful parsing must not claim configuration was actually applied when it was dropped. +- Change Agent turn/cancel state machines to address a timing observation: outside this + migration; preserve existing assertions, record the observed failure and retest. + +## Dependencies + +Client base dependency `jsonschema>=4.23,<5`; no new cloud resource, model adapter or release. +Producer: [template contract PR310](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/pull/310), +final [fad843f](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/commit/fad843f60881b405cb386537337817e5144892bc). +Previous narrow API patch: [SDK PR163](https://github.com/Alibaba-Agent-Sandbox/python-sdk/pull/163). +Cross-repository delivery: [templates309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309). +SDK-owned generic launch JSON/vectors must be synchronized by CLI/Registry consumers after +this scoped source is committed; old private planner projections are not the new public YAML. + +## Test Strategy + +Run shared owner admission vectors with the installed SDK's real JSON Schema validator. +Exercise launch through the actual Express HTTP serializer with an HTTP mock, checking +runtime specs, TTL/defaults/overrides, env, metadata and named Volume mounts rather than +field existence. Check no env values in plans/journals/settings/ownership receipts, no Volume +DELETE, malformed or unresolved input rejected before resource creation, and unchanged +cleanup on failure. Use actual temporary SQLite and PostgreSQL for Session snapshots. +Run both package suites and full local CI, packaging/fresh installs, type/lint/format, +health/hygiene, source hash verification and secret/diff checks. Cloud/model end-to-end +acceptance remains a separate required gate of the encompassing delivery. + +## Acceptance criteria + +- Canonical source bytes and lock match the reviewed producer commit. +- Prepared literals work without CLI installation or source building; no silent expression evaluation. +- Actual creation bodies contain declared instance fields and correctly prioritized overrides. +- Managed native TTL equals the Session lifetime snapshot, including default behavior. +- Custom env values exist only in Worker private memory and actual guest requests. +- Reserved controls and protected directories cannot be overridden; borrowed Volumes survive cleanup. +- Discovery and bilingual SDK guidance use v1; independent server/Provider APIs remain intact. +- Local and remote required checks, skips and the outstanding real-cloud gate are reported separately. + +## Evidence + +[Verification](../../../evidence/verify/2026-11-12-template-v1-runtime.md). + +[Ports and final gate follow-up](../../../evidence/verify/2026-11-12-template-v1-runtime-ports.md). + +[Final local gate results](../../../evidence/verify/2026-11-12-template-v1-runtime-final-local.md). diff --git a/.agents/notes/proposed/feature/2026-11-12-template-yaml-core-reader.md b/.agents/notes/proposed/feature/2026-11-12-template-yaml-core-reader.md new file mode 100644 index 00000000..b046e0de --- /dev/null +++ b/.agents/notes/proposed/feature/2026-11-12-template-yaml-core-reader.md @@ -0,0 +1,64 @@ +# Decision: share YAML1.2 Core admission for public v1 files + +Status: proposed + +## Problem + +The Node and Python v1 readers disagree before the shared object schema runs. Python's default +safe loader converts plain `on` to boolean and dates to non-JSON objects, while accepting +repeated keys, aliases and UTF16. Six public SDK behavior regressions reproduce these issues +in prepared launch, cached discovery and registry file loading. + +## Decision + +Copy the template owner's pure `parse_yaml_v1` and raw YAML text vectors exactly with the +immutable source lock. Use `yamlcore==0.1.1`, a small PyYAML-dependent Core1.2 loader, instead of +maintaining another resolver. Limit file reads to1MiB+1 bytes before parsing. Apply the same +strict UTF8, duplicate/alias/tag and finite JSON/string-key admission in every public v1 file +entry before validation, journaling or resource creation. + +## API Design + +An internal bounded I/O helper returns original bytes plus the shared parsed object. The launch +path preserves its existing original-byte identity; dict/resolved JSON input is unchanged. +Public template registry and cache discovery use the same helper. Native Devs expressions +remain literal strings here and require the real Devs parser for evaluation. Unrelated provider +configuration/catalog readers retain their existing contracts. + +## Alternatives considered + +- Ask users to quote scalars differently for Python: rejected because the same public YAML + must retain the same meaning across consumers. +- Three custom handwritten resolver implementations: rejected as duplicate specification. +- A complete new YAML framework or installing the CLI: unnecessary for this bounded file + admission; use the reviewed minimal Core loader and existing schema. +- Read the whole file before checking size: rejected because it does not bound input allocation. + +## Dependencies + +Base client dependency `yamlcore==0.1.1`, alongside existing PyYAML. Shared reader and raw +vectors are pinned to the template-owner commit recorded in `template_contract/source.json`. +Part of [delivery309](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309) +and the [canonical contract PR310](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/pull/310). +No package release or new runtime service is introduced. Managed Server consumes compiledJSON, +so this file-entry fix does not require another Guest/model/cloud matrix. + +## Test Strategy + +Run actual raw text vectors through the real installed Core loader. Assert all three public +paths preserve scalar values; invalid encoding, repeated keys, aliases and oversized input fail +before Sandbox/journal creation. Check error strings omit source values. Verify immutable owner +bytes, actual built wheel contents/dependency metadata and fresh environment installation with +pipcheck. Run relevant discovery/launch/schema tests and repository static/health gates. + +## Acceptance criteria + +- Owner lock and bundled reader/vector bytes match the immutable producer. +- Public file paths agree with Node's shared text cases and keep literal env values unchanged. +- Bounded strict parsing fails before resource or journal side effects without source leakage. +- A fresh installed client wheel includes the actual reader, vectors and Core dependency. +- Historical full-suite scope remains explicit; final-head remote CI/cloud gates are separate. + +## Evidence + +[Verification](../../../evidence/verify/2026-11-12-template-yaml-core-reader.md). diff --git a/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md b/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md new file mode 100644 index 00000000..23153ead --- /dev/null +++ b/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md @@ -0,0 +1,70 @@ +# Decision: Retain python-sdk Agents API delivery operations + +Status: proposed + +## Problem + +Runtime evidence exists in this owner, while the capability/alignment matrix and the exact official-SDK interoperability probe were retained only outside the repository. Maintainers need both the operations and the precise limits in a standalone clone. + +## Decision + +Keep SDK/runtime verification and OpenAI compatibility observations in this .agents directory. Preserve exact test revisions and earlier failures. Retain the original small offline interoperability source and typed outcomes, without adding OpenAI as a product dependency or copying its distribution binary. + +## API Design + +No SDK or server API change. The candidate public contract is v1; the Managed profile and the broad local server retain distinct contracts. + +### Owner verification operations + +```sh +make dev +make test +make ci +bash scripts/check-repo-health.sh --quiet +make hygiene +``` + +On a shared same-UID host, the recorded full validation used a real PID/proc namespace via `unshare -Upf --map-current-user --mount-proc` while retaining a non-root UID. Preserve the process-safety scanner and assertions. A same-UID process outside the owned execution scope is not proof of safe reuse. Root PostgreSQL restrictions, missing namespace support and old CLI prerequisites must be reported, not hidden as passes. + +Synchronize the canonical files with scripts/sync_template_v1_contract.py using the exact producer revision and check the source lock. Its source_checkout argument names a reviewed producer checkout; --revision selects its full immutable commit, and --check verifies without rewriting. Build both wheels and verify fresh client-only and server-extra installations; match source bytes, not the unchanged a7 metadata label. Custom instance values reach the owning Worker, while Session/state/receipts keep only permitted safe values and environment names. + +### Capability and compatibility review + +Managed supports the admitted Codex/Qwen text, Session/Turn, durable submissions/events, fixed file/artifact and close/delete subset. Busy input that cannot steer is rejected; the explicit submission queue is an extension. Unsupported external tools/MCP/subagents/automations are not silently advertised. The official hosted harness/self-hosted exec-server topology is not the same as a full CLI/model/tool loop in Sandbox. + +Saved Agent plus inline Session overrides are officially valid; do not reintroduce a one-of guard into Managed. The broad local server still has a separate saved-Agent/override difference and different persistence/input behavior. Adapter classes are not evidence that HTTP admits Claude/Cursor. + +The [retained SDK probe](../../../evidence/verify/2026-11-12-agents-api-interoperability.md) uses official openai3.49.0 against real loopback Managed HTTP/SQLite, with no Worker/model/cloud. Default create/retrieve/list decoding passed. Strict validation still fails on real Agent/harness/environment differences. The public integer timestamp/empty-action/Vault projection fixes only its verified fields; never fabricate a remote_url, inline saved-Agent identity or effective model settings to satisfy a decoder. + +### Existing evidence + +- [Full local gates and exact source scope](../../../evidence/verify/2026-11-12-template-v1-runtime-final-local.md). +- [Projection](../../../evidence/verify/2026-11-12-managed-session-projection.md), [Core reader and installed wheel](../../../evidence/verify/2026-11-12-template-yaml-core-reader.md), [standalone policy documentation](../../../evidence/verify/2026-11-12-standalone-policy-docs.md). + +The final Server image uses SDK source b988d2a76057b6202d836ec329cc0bd90a572b26; c4d43a2 only changes later documentation. Image/model evidence belongs to the template owner and does not establish FC ingress, old Worker routing or cloud cleanup. + +## Alternatives considered + +A root-only compatibility report can be lost between sessions and makes default SDK decoding look stronger than it is. Adding OpenAI to the product dependencies or implementing a second hosted harness is unnecessary. Keep the narrow probe and truthful matrix in engineering records instead. + +## Dependencies + +Tracking: https://github.com/Alibaba-Agent-Sandbox/awesome-templates/issues/309. Cross-repository links refer to owner branches +and do not assert that pending work is merged or published. + +- [awesome-templates owner operations](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/feat/template-v1-migration-20261112/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md). +- [agent-sandbox owner operations](https://github.com/Alibaba-Agent-Sandbox/agent-sandbox/blob/feat/template-v1-parser-20261112/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md). +- [registry owner operations](https://github.com/Alibaba-Agent-Sandbox/registry/blob/feat/template-v1-registry-20261112/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md). +- [blogs owner operations](https://github.com/Alibaba-Agent-Sandbox/blogs/blob/docs/agents-api-owner-records-20261112/.agents/notes/proposed/process/2026-11-12-agents-api-delivery-operations.md). + +## Test Strategy + +Validate links, note shape, retained source/result hashes, probe Python syntax and record hygiene. Historical full-suite counts, final deltas and remote matrix are separate facts. Copying the probe does not rerun it; a future probe run needs its own dated result. + +## Acceptance criteria + +A standalone SDK checkout explains what was tested, how to repeat local gates, what remains unaligned, and which cloud/model operations are still required. No record implies strict official SDK compatibility or a new package publication. + +## Evidence + +- [Record-placement verification](../../../evidence/verify/2026-11-12-agents-api-delivery-operations.md). diff --git a/AGENTS.md b/AGENTS.md index 8c3ff7c5..c66c7b58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,24 +169,18 @@ Every exception carries: `code`, `message`, `suggestion`, `docs_url` (populated ### Template startup and operation model -Templates use strict specification 5 with the complete `provides` service map, -`start_cmd` and `ready_cmd`. There is no workflow selector or named startup type. -Canonical `contracts/launch-v1.json` and `launch-vectors.json` have byte-identical -package copies. Historical workflow/receipt files remain for old evidence only; -they do not certify the new launcher. Provider operation declarations remain exact. - -`api/template.py` exposes `launch_template` for prepared references, journals owned -resources, starts the template once and checks all SandboxServer protocol v2 services. Python -does not build source. Use `session.services` for all resolved clients and -`session.servers` for SandboxServer clients. Do not block startup on an unrelated -interpreter operation. See [the launch guide](docs/en/guide/template-workflows.md). +Templates use the canonical `specVersion: 1` contract, `sandbox.services`, +`sandbox.start` and `sandbox.ready`. The immutable owner source/schema lock is in +`client-sdk/src/agent_sandbox/template_contract/source.json`; synchronize those bytes, +never maintain another parser or invent an expression evaluator. Native Devs parses +expressions; Python consumes literals or an already resolved v1 snapshot. -### Template Definition - -Use `template.yaml`, `spec_version: 5`, `provides`, startup/readiness, and the existing -server policy. Reject workflow_contract_version/workflows/default_workflow with -migration guidance. Named application commands are registered by the server, never -created from YAML. Template discovery grants no default runtime operations. +Canonical `contracts/launch-v1.json` and `launch-vectors.json` have byte-identical +package copies. `launch_template` consumes prepared references and instance values; +Python does not build source. Serving/application inputs require the deployment +orchestrator. Server policy validation remains an independent runtime API. +Managed instance settings exclude every environment value; only Worker private +configuration supplies those values, with reserved controls protected. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index a202261e..223eea3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Fixed +- Read public v1 template files with shared YAML1.2 Core rules, strict UTF-8 and + bounded input; reject duplicate keys/aliases without exposing source values. / + 公开 v1 模板文件统一采用 YAML1.2 Core、严格 UTF-8 与大小限制,拒绝重复键和别名,错误不泄露原文值。 + +- Return managed Session timestamps as integer Unix seconds and truthful empty + `required_actions` / `vault_ids` arrays without changing internal clock precision. / + 托管 Session 时间输出为整数秒,返回实际为空的待办动作与 Vault 列表,内部时钟精度保留。 + - Accept the optional `type: "message"` discriminator for managed user-text input, validate explicitly supplied saved Agent IDs without coercion or inline fallback, and distinguish rate-limit and unavailable-service error categories. Saved Agent @@ -19,10 +27,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Documentation +- Describe optional SandboxServer access policy as a standalone JSON/YAML runtime + document and remove obsolete template-root server/automatic-compilation instructions. / + 可选的 SandboxServer 访问策略改为独立 JSON/YAML 运行时文档,移除旧清单根字段及自动编译说明。 + - Synchronize CLI-dependent SDK documentation, HTTP MCP template deployment and source-acquisition error guidance. / 同步 SDK 文档中的 CLI 用法、HTTP MCP 模板部署与源码获取错误指引。 ### Changed +- Preserve additional `sandbox.ports` alongside service ports. Prepared sessions + expose Provider endpoint descriptors without inventing clients or readiness checks; + gateway headers remain private in memory. / 保留额外端口声明;启动会话返回 + Provider 端点描述,不创建虚构客户端或就绪检查,网关请求头只在内存中。 + +- Consume canonical `specVersion: 1` resource documents in prepared Python launch + and discovery. Preserve instance TTL/specs/Volumes/environment/metadata and explicit + API overrides; reject unresolved native expressions before creation. Managed + Sessions snapshot safe instance settings and receive custom environment values + only through private Worker configuration. / Python 已准备模板启动与发现改为 + canonical v1,实际承接实例参数与显式覆盖;未解析表达式在创建前拒绝。 + 托管 Session 保存安全实例快照,自定义环境值只通过 Worker 私有配置注入。 + - Align current English and Chinese CLI guides with template authoring, named deployment lifecycle and Provider operations; preserve the SDK’s independent clients and instance methods. - The client no longer installs the local server runtime by default. Use the diff --git a/client-sdk/pyproject.toml b/client-sdk/pyproject.toml index e8ef6420..90363bee 100644 --- a/client-sdk/pyproject.toml +++ b/client-sdk/pyproject.toml @@ -40,6 +40,8 @@ dependencies = [ "pydantic>=2.14.0", "python-dotenv>=1.0", "pyyaml>=6.0.3", + "yamlcore==0.1.1", + "jsonschema>=4.23,<5", "protobuf>=5", "tomli>=2.0; python_version < '3.11'", ] @@ -110,8 +112,9 @@ markers = [ [tool.ruff] target-version = "py310" line-length = 100 +force-exclude = true src = ["src", "tests"] -extend-exclude = ["*_pb2.py", "*_pb2_grpc.py", "src/agent_sandbox/_standard", "src/agent_sandbox/code_interpreter"] +extend-exclude = ["src/agent_sandbox/template_contract/normalize.py", "*_pb2.py", "*_pb2_grpc.py", "src/agent_sandbox/_standard", "src/agent_sandbox/code_interpreter"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "TCH"] @@ -145,3 +148,9 @@ follow_imports = "skip" module = ["agent_sandbox._standard.*", "agent_sandbox.code_interpreter.*"] follow_imports = "skip" ignore_errors = true + +# Immutable owner-maintained normalizer; validate byte hashes and shared vectors. +[[tool.mypy.overrides]] +module = ["agent_sandbox.template_contract.normalize", "jsonschema.*"] +ignore_errors = true +ignore_missing_imports = true diff --git a/client-sdk/src/agent_sandbox/api/capability.py b/client-sdk/src/agent_sandbox/api/capability.py index ecb785f2..95e87fbc 100644 --- a/client-sdk/src/agent_sandbox/api/capability.py +++ b/client-sdk/src/agent_sandbox/api/capability.py @@ -9,6 +9,7 @@ from agent_sandbox.models.errors import TemplateParseError from agent_sandbox.models.template import SandboxTemplate +from agent_sandbox.template_contract import _read_yaml_document TEMPLATE_CACHE_DIR = Path.home() / ".abx" / "templates" @@ -21,21 +22,13 @@ class ResolvedCapabilities(BaseModel): def parse_template_data(data: dict[str, Any]) -> SandboxTemplate: - """Validate specification 5, preserving unrelated application topology metadata.""" - normalized = dict(data) - resources = normalized.get("resources", {}) or {} - if "application" not in normalized and "cpu" in resources and "cpu_count" not in normalized: - normalized["cpu_count"] = resources["cpu"] - if "application" not in normalized and "memory" in resources and "memory_mb" not in normalized: - normalized["memory_mb"] = resources["memory"] - return SandboxTemplate.model_validate(normalized) + """Validate canonical v1 intent and retain its native topology unchanged.""" + return SandboxTemplate.model_validate(data) def _read(path: Path) -> SandboxTemplate: - import yaml - try: - data = yaml.safe_load(path.read_text(encoding="utf-8")) + _, data = _read_yaml_document(path) if not isinstance(data, dict): raise ValueError("template must be a mapping") return parse_template_data(data) @@ -62,12 +55,10 @@ async def resolve_capabilities_for( if local_yaml_path is not None: template = _read(Path(local_yaml_path)) return ResolvedCapabilities(capabilities=template.discovery_capabilities(), matched=True) - import yaml - for template_id in dict.fromkeys(t for t in template_ids if t): for path in sorted(TEMPLATE_CACHE_DIR.rglob("template.yaml")): try: - data = yaml.safe_load(path.read_text(encoding="utf-8")) + _, data = _read_yaml_document(path) except Exception: if path.parent.name == template_id: _read(path) @@ -76,7 +67,7 @@ async def resolve_capabilities_for( if path.parent.name == template_id: _read(path) continue - aliases = data.get("aliases", []) + aliases = data.get("sandbox", {}).get("template", {}).get("aliases", []) names = [data.get("name"), data.get("alias"), path.parent.name] if isinstance(aliases, list): names.extend(aliases) diff --git a/client-sdk/src/agent_sandbox/api/sandbox.py b/client-sdk/src/agent_sandbox/api/sandbox.py index c7da84f3..a841409f 100644 --- a/client-sdk/src/agent_sandbox/api/sandbox.py +++ b/client-sdk/src/agent_sandbox/api/sandbox.py @@ -405,24 +405,21 @@ def server_at( timeout=timeout, ) - async def application_client( + async def application_endpoint( self, port: int = 9000, *, - token: str | None = None, traffic_access_token: str | None = None, - timeout: float = 30.0, - ) -> ServerClient: - """Resolve application HTTP independently of the sandbox control/data transport. + ) -> dict[str, Any]: + """Resolve a Provider port endpoint without creating or probing a service client. - The provider supplies this sandbox's port URL and minimal gateway headers. - Application credentials are explicit and never inherited from API keys. - traffic_access_token overrides the stored gateway token for this client - only. No connect, resume or lifetime renewal is performed. + Returns the selected Provider's URL, gateway headers and addressing metadata. + This does not start the application or establish native TCP reachability. + Control-plane keys are never copied into gateway headers. No connect, + resume or lifetime renewal is performed. """ from datetime import datetime, timezone - from agent_sandbox.api.server_client import ServerClient from agent_sandbox.protocol.port_contract import PortContext from agent_sandbox.providers.registry import get_provider @@ -468,13 +465,34 @@ async def application_client( path_mode = descriptor.get("path_mode") if not isinstance(path_mode, str): raise ValueError("Application endpoint path mode is required") + return {**descriptor, "headers": dict(headers)} + + async def application_client( + self, + port: int = 9000, + *, + token: str | None = None, + traffic_access_token: str | None = None, + timeout: float = 30.0, + ) -> ServerClient: + """Resolve an application endpoint and create its explicit HTTP client. + + Gateway and application authentication have separate audiences. The token + authenticates the application; traffic_access_token overrides only the + Provider gateway token for this client. + """ + from agent_sandbox.api.server_client import ServerClient + + descriptor = await self.application_endpoint( + port, traffic_access_token=traffic_access_token + ) return ServerClient( - url, - headers=headers, + descriptor["url"], + headers=descriptor["headers"], token=token, timeout=timeout, - path_mode=path_mode, - expires_at=expires, + path_mode=descriptor["path_mode"], + expires_at=descriptor.get("expires_at"), ) async def start_server( diff --git a/client-sdk/src/agent_sandbox/api/template.py b/client-sdk/src/agent_sandbox/api/template.py index ae7675fc..82cb887b 100644 --- a/client-sdk/src/agent_sandbox/api/template.py +++ b/client-sdk/src/agent_sandbox/api/template.py @@ -14,6 +14,7 @@ from agent_sandbox.api.sandbox import Sandbox from agent_sandbox.models.process import ProcessResult +from agent_sandbox.template_contract import _read_yaml_document, compile_literal_document from agent_sandbox.transport.streaming import StreamReader from agent_sandbox.workflows.identity import bytes_digest from agent_sandbox.workflows.launch import plan_template_launch, validate_template_manifest @@ -59,8 +60,10 @@ def __init__( sandbox: Sandbox | None, services: dict[str, ServerClient], journal: _Journal, + endpoints: dict[int, dict[str, Any]] | None = None, ) -> None: self.plan, self.sandbox, self.services = plan, sandbox, services + self.endpoints = endpoints or {} self.servers = { name: client for name, client in services.items() @@ -192,7 +195,13 @@ async def launch_template( application_token: str | None = None, provider_options: dict[str, Any] | None = None, envs: dict[str, str] | None = None, - timeout: int = 300, + timeout: int | None = None, + cpu: int | None = None, + memory: int | None = None, + disk: int | None = None, + gpu: str | None = None, + volume_mounts: dict[str, str] | None = None, + metadata: dict[str, str] | None = None, readiness_timeout: float = 30, journal_path: str | Path | None = None, ) -> TemplateSession: @@ -201,39 +210,29 @@ async def launch_template( Python never builds source projects. Prepare sources with the CLI and pass the resulting reference, or declare an image/native template in the manifest. All services are returned. SandboxServer services are checked with health and - discovery; other protocols use the template's ready_cmd for readiness. + discovery; other protocols use the template's sandbox.ready for readiness. """ + from agent_sandbox.providers.base import Provider as ProviderContract from agent_sandbox.providers.registry import builtin_provider, get_provider if isinstance(manifest, (str, Path)): - import yaml - - manifest_bytes = Path(manifest).read_bytes() - snapshot = yaml.safe_load(manifest_bytes) + manifest_bytes, snapshot = _read_yaml_document(manifest) else: snapshot = copy.deepcopy(manifest) manifest_bytes = json.dumps(snapshot, sort_keys=True).encode() snapshot = validate_template_manifest(snapshot) - if snapshot.get("application"): + if snapshot.get("deployment") or snapshot.get("application"): raise TemplateLaunchError("deployment-orchestrator-required", "before-create") - resolved = reference or snapshot.get("image") or snapshot.get("template_id") + template = snapshot.get("sandbox", {}).get("template", {}) + resolved = reference or template.get("image") or template.get("reference") if not isinstance(resolved, str) or not resolved.strip(): raise TemplateLaunchError("prepare-source-with-cli-and-pass-reference", "before-prepare") - if snapshot.get("image") and not re.fullmatch( + if template.get("image") and not re.fullmatch( r"(?:[A-Za-z0-9][A-Za-z0-9._:/-]*@)?sha256:[a-f0-9]{64}", resolved ): raise TemplateLaunchError("immutable-image-reference-required", "before-create") if readiness_timeout <= 0: raise ValueError("readiness_timeout must be positive") - effective_env = { - key: value or os.environ.get(key, "") for key, value in snapshot.get("env", {}).items() - } - effective_env.update(envs or {}) - if application_token is not None: - effective_env["ABX_SERVER_TOKEN"] = application_token - token = effective_env.get("ABX_SERVER_TOKEN") or None - if "ABX_SERVER_TOKEN" in effective_env and not token: - raise TemplateLaunchError("application-token-required", "before-create") prov = get_provider(provider) if isinstance(provider, str) else provider plan = plan_template_launch(snapshot, prov.describe(), "python-provider", prepared=True) if not plan["supported"]: @@ -241,6 +240,50 @@ async def launch_template( plan.get("reason") or ("missing-operations: " + ", ".join(plan["missing_operations"])), "before-create", ) + # Explicit API values override admitted intent before resolved validation. + # This replaces values; it does not interpret any native expression. + effective = copy.deepcopy(snapshot) + declared_instance = effective["sandbox"]["instance"] + declared_instance["envs"] = {**declared_instance["envs"], **(envs or {})} + declared_instance["metadata"] = {**declared_instance["metadata"], **(metadata or {})} + if application_token is not None: + declared_instance["envs"]["ABX_SERVER_TOKEN"] = application_token + for key, value in { + "timeout": timeout, + "cpu": cpu, + "memory": memory, + "disk": disk, + "gpu": gpu, + "volume_mounts": volume_mounts, + }.items(): + if value is not None: + declared_instance[key] = value + validator = getattr(prov, "validate_template_input", None) + if getattr(validator, "__func__", None) is ProviderContract.validate_template_input: + validator = None + + def validate_selected(stage: str, value: Any, provider_id: str) -> Any: + if provider_id != prov.id or not callable(validator): + raise ValueError("Template validation requires the selected Provider") + return validator(stage, value) + + compiled = compile_literal_document( + effective, + provider=prov.id, + validate_provider=validate_selected if callable(validator) else None, + ) + instance = copy.deepcopy(compiled["sandbox"]["instance"]) + lifecycle = instance.pop("lifecycle", {}) + if lifecycle.get("onTimeout", "kill") != "kill" or lifecycle.get("autoResume", False): + raise TemplateLaunchError("instance-lifecycle-unimplemented", "before-create") + effective_env = instance.pop("envs") + effective_metadata = instance.pop("metadata") + token = effective_env.get("ABX_SERVER_TOKEN") or None + if "ABX_SERVER_TOKEN" in effective_env and not token: + raise TemplateLaunchError("application-token-required", "before-create") + supported_instance = {"timeout", "cpu", "memory", "disk", "gpu", "volume_mounts"} + if set(instance) - supported_instance: + raise TemplateLaunchError("instance-field-unimplemented", "before-create") run_id = uuid4().hex journal = _Journal( Path(journal_path) if journal_path else Path.home() / ".abx" / "runs" / f"{run_id}.jsonl" @@ -260,6 +303,7 @@ async def launch_template( creation_uncertain = False sandbox: Sandbox | None = None clients: dict[str, ServerClient] = {} + endpoints: dict[int, dict[str, Any]] = {} session: TemplateSession | None = None try: if prov.content_digest() != provider_digest: @@ -271,10 +315,8 @@ async def launch_template( provider=prov.id if prov is builtin_provider() else provider, provider_options=provider_options, envs=effective_env, - cpu=snapshot.get("cpu_count", snapshot.get("resources", {}).get("cpu")), - memory=snapshot.get("memory_mb", snapshot.get("resources", {}).get("memory")), - timeout=timeout, - metadata={"abx_template_run": run_id}, + **instance, + metadata={**effective_metadata, "abx_template_run": run_id}, on_created=lambda sandbox_id: journal.append( "created", sandbox_id=sandbox_id, provider_id=prov.id ), @@ -294,10 +336,10 @@ async def launch_template( journal.append("created", sandbox_id=sandbox.id, provider_id=prov.id) if prov.content_digest() != provider_digest: raise TemplateLaunchError("provider-content-changed", "before-start") - if snapshot.get("start_cmd"): - await _dispatch_start(sandbox, snapshot["start_cmd"], readiness_timeout) + if compiled.get("start_cmd"): + await _dispatch_start(sandbox, compiled["start_cmd"], readiness_timeout) journal.append("started", sandbox_id=sandbox.id) - if snapshot.get("ready_cmd"): + if compiled.get("ready_cmd"): loop = asyncio.get_running_loop() deadline = loop.time() + readiness_timeout while True: @@ -307,7 +349,7 @@ async def launch_template( try: result = await asyncio.wait_for( sandbox.commands.run( - snapshot["ready_cmd"], timeout=max(1, math.ceil(remaining)) + compiled["ready_cmd"], timeout=max(1, math.ceil(remaining)) ), timeout=remaining, ) @@ -318,6 +360,9 @@ async def launch_template( if result.exit_code == 0: break await asyncio.sleep(min(0.25, max(0, deadline - loop.time()))) + service_ports = {service["port"] for service in plan["services"].values()} + for port in sorted(set(compiled["sandbox"].get("ports", [])) - service_ports): + endpoints[port] = await sandbox.application_endpoint(port) for name, service in plan["services"].items(): client = await sandbox.application_client(service["port"], token=token) clients[name] = client @@ -332,11 +377,11 @@ async def launch_template( if discovery.api_version != service["version"]: raise TemplateLaunchError("protocol-mismatch: " + name, "before-invoke") journal.append("protocol-checked", service=name, api_version=discovery.api_version) - session = TemplateSession(plan, sandbox, clients, journal) + session = TemplateSession(plan, sandbox, clients, journal, endpoints) return session except BaseException: if session is None: - session = TemplateSession(plan, sandbox, clients, journal) + session = TemplateSession(plan, sandbox, clients, journal, endpoints) session.creation_uncertain = creation_uncertain await session.close() raise diff --git a/client-sdk/src/agent_sandbox/managed_agents/__main__.py b/client-sdk/src/agent_sandbox/managed_agents/__main__.py index 51d589ff..ac534b03 100644 --- a/client-sdk/src/agent_sandbox/managed_agents/__main__.py +++ b/client-sdk/src/agent_sandbox/managed_agents/__main__.py @@ -128,7 +128,17 @@ def serve(args: argparse.Namespace, ca_file: str | None) -> None: provider = load_document("ABX_MANAGED_PROVIDER_CONFIG") model = load_document("ABX_MANAGED_MODEL_CONFIG", required=args.mode == "worker") assert provider is not None - backend = ExpressBackend(provider, model) + from agent_sandbox.models.managed_instance import worker_environment + + private_env = ( + worker_environment( + load_document("ABX_MANAGED_SANDBOX_ENV", required=False) or {}, + config.instance_env_names, + ) + if args.mode == "worker" + else {} + ) + backend = ExpressBackend(provider, model, instance_env=private_env) worker = ManagedWorker(service, backend) sender = None if args.mode == "dispatcher" and not args.local: diff --git a/client-sdk/src/agent_sandbox/managed_agents/models.py b/client-sdk/src/agent_sandbox/managed_agents/models.py index e796c249..8d4c2153 100644 --- a/client-sdk/src/agent_sandbox/managed_agents/models.py +++ b/client-sdk/src/agent_sandbox/managed_agents/models.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import hashlib import json from dataclasses import dataclass, field @@ -102,6 +103,8 @@ class ManagedConfig: workers: dict[str, str] = field(default_factory=dict) model_env_allowlist: list[str] = field(default_factory=list) expected_state_id: str | None = None + instance: dict[str, Any] = field(default_factory=dict) + instance_env_names: list[str] = field(default_factory=list) sessions: dict[str, int] = field( default_factory=lambda: { "idleTimeoutSeconds": 900, @@ -135,6 +138,16 @@ class ManagedConfig: ) def __post_init__(self) -> None: + from agent_sandbox.models.managed_instance import validate_env_names, validate_instance + + object.__setattr__(self, "sessions", copy.deepcopy(self.sessions)) + instance = validate_instance( + self.instance, self.workspace, self.sessions["maxLifetimeSeconds"] + ) + # Public v1 has one TTL; the private Session budget is derived from it. + self.sessions["maxLifetimeSeconds"] = instance["timeout"] + object.__setattr__(self, "instance", instance) + object.__setattr__(self, "instance_env_names", validate_env_names(self.instance_env_names)) if ( not isinstance(self.revision, str) or not self.revision @@ -170,6 +183,8 @@ def __post_init__(self) -> None: not isinstance(self.port, int) or isinstance(self.port, bool) ) or not 1 <= self.port <= 65535: raise ValueError("The bridge port must be 1-65535") + if self.sessions["idleTimeoutSeconds"] > self.sessions["maxLifetimeSeconds"]: + raise ValueError("The idle timeout must fit the Session lifetime") if self.sessions["maxConcurrentTurnsPerSession"] != 1: raise ValueError("Managed Sessions execute one turn at a time") if ( @@ -209,6 +224,8 @@ def from_dict(cls, value: dict[str, Any]) -> ManagedConfig: "workers", "modelEnvAllowlist", "expectedStateId", + "instance", + "instanceEnvNames", "sessions", "limits", "storage", @@ -252,4 +269,14 @@ def from_dict(cls, value: dict[str, Any]) -> ManagedConfig: raise ValueError("Unsupported managed lifecycle policy") mapping = {k: v for k, v in value[key].items() if k in permitted} kwargs[key] = {**getattr(defaults, key), **mapping} + for wire, native in (("instance", "instance"), ("instanceEnvNames", "instance_env_names")): + if wire in value: + kwargs[native] = value[wire] + if ( + isinstance(value.get("instance"), dict) + and "timeout" in value["instance"] + and "maxLifetimeSeconds" in value.get("sessions", {}) + and value["instance"]["timeout"] != value["sessions"]["maxLifetimeSeconds"] + ): + raise ValueError("Compiled Session lifetime must equal instance.timeout") return cls(**kwargs) diff --git a/client-sdk/src/agent_sandbox/managed_agents/service.py b/client-sdk/src/agent_sandbox/managed_agents/service.py index 7427cfbd..47d8443c 100644 --- a/client-sdk/src/agent_sandbox/managed_agents/service.py +++ b/client-sdk/src/agent_sandbox/managed_agents/service.py @@ -3,6 +3,7 @@ from __future__ import annotations import base64 +import copy import hashlib import hmac import json @@ -71,6 +72,11 @@ def public(row: dict[str, Any]) -> dict[str, Any]: """Remove all private control/state fields before serializing a public object.""" out = {k: v for k, v in row.items() if not k.startswith("_")} + if row.get("_kind") == "session": + for key in ("created_at", "last_active_at"): + out[key] = int(row[key]) + out["required_actions"] = [] + out["vault_ids"] = [] if row.get("_kind") == "session" and isinstance(row.get("environment"), dict): out["environment"] = { **row["environment"], @@ -482,6 +488,8 @@ def create_session( "workspace": self.config.workspace, "port": self.config.port, "modelEnvAllowlist": self.config.model_env_allowlist, + "instance": copy.deepcopy(self.config.instance), + "instanceEnvNames": list(self.config.instance_env_names), "sessions": self.config.sessions, "limits": self.config.limits, "storage": self.config.storage, diff --git a/client-sdk/src/agent_sandbox/models/managed_instance.py b/client-sdk/src/agent_sandbox/models/managed_instance.py new file mode 100644 index 00000000..0378639f --- /dev/null +++ b/client-sdk/src/agent_sandbox/models/managed_instance.py @@ -0,0 +1,95 @@ +"""Managed-only instance admission; secret values never enter Session settings.""" + +from __future__ import annotations + +import copy +import re +from typing import Any + +from agent_sandbox.models.sandbox import validate_volume_mounts +from agent_sandbox.template_contract import compile_literal_document + +_STATE_DIRECTORY = "/home/user/.agent-sandbox" +_RESERVED_METADATA = frozenset({"abx-managed", "abx-session", "abx-revision"}) + + +def validate_env_names(names: Any) -> list[str]: + """Accept only explicitly declared custom names outside runtime/model controls.""" + if not isinstance(names, list) or any(not isinstance(name, str) for name in names): + raise ValueError("Managed instanceEnvNames must be a list of environment names") + if len(names) != len(set(names)): + raise ValueError("Managed instanceEnvNames must not contain duplicates") + for name in names: + if ( + not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) + or name + in { + "HOME", + "PATH", + "SHELL", + "USER", + "LOGNAME", + "ANTHROPIC_API_KEY", + "DASHSCOPE_API_KEY", + } + or name.startswith(("ABX_", "OPENAI_", "QWEN_", "CODEX_", "PYTHON", "LD_")) + ): + raise ValueError( + "Managed custom environment names cannot override runtime/model controls" + ) + return list(names) + + +def validate_instance(value: Any, workspace: str, lifetime: int) -> dict[str, Any]: + """Admit safe Express fields and protect exclusive runtime directories.""" + if not isinstance(value, dict): + raise ValueError("Managed instance must be an object") + value = copy.deepcopy(value) + if "envs" in value: + raise ValueError( + "Managed instance environment values belong only in the Worker secret source" + ) + value.setdefault("timeout", lifetime) + compiled = compile_literal_document( + { + "specVersion": 1, + "name": "managed", + "sandbox": { + "provider": "alicloud-express", + "template": {"reference": "prepared"}, + "instance": value, + }, + }, + provider="alicloud-express", + ) + instance: dict[str, Any] = compiled["sandbox"]["instance"] + instance.pop("envs") + lifecycle = instance.get("lifecycle", {}) + if lifecycle.get("onTimeout", "kill") != "kill" or lifecycle.get("autoResume", False): + raise ValueError("Managed Sessions require kill-on-timeout without automatic resume") + if _RESERVED_METADATA.intersection(instance["metadata"]): + raise ValueError("Managed instance metadata cannot override resource ownership") + mounts = validate_volume_mounts(instance.get("volume_mounts", {})) + for mount in mounts: + if any( + mount == protected + or mount.startswith(protected + "/") + or protected.startswith(mount + "/") + for protected in (workspace.rstrip("/"), _STATE_DIRECTORY) + ): + raise ValueError( + "Managed volume mounts cannot overlap the workspace or private runtime state" + ) + return instance + + +def worker_environment(value: Any, names: list[str]) -> dict[str, str]: + """Resolve only a supplied private map, without process-wide ambient forwarding.""" + names = validate_env_names(names) + if ( + not isinstance(value, dict) + or set(value) != set(names) + or any(not isinstance(item, str) or "\x00" in item for item in value.values()) + ): + raise ValueError("Worker sandbox environment must exactly match instanceEnvNames") + return dict(value) diff --git a/client-sdk/src/agent_sandbox/models/template.py b/client-sdk/src/agent_sandbox/models/template.py index 383992c3..60eebce1 100644 --- a/client-sdk/src/agent_sandbox/models/template.py +++ b/client-sdk/src/agent_sandbox/models/template.py @@ -4,7 +4,7 @@ from typing import Any -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator # --------------------------------------------------------------------------- # Standard capability vocabulary @@ -135,11 +135,10 @@ def groups(value: Any) -> None: class SandboxTemplate(BaseModel): - """template.yaml 的 Pydantic 模型。 + """Read-only discovery view of canonical v1 intent, without expression evaluation. - 构建相关字段(system_packages, python_packages, node_packages, - commands, copy_files)已移除——实际构建流程直接使用 Dockerfile + - 语言原生依赖文件(requirements.txt / package.json)。 + Derived fields describe declared services and preparation properties only; + they never grant Provider operations or resolve runtime secrets. """ name: str @@ -161,21 +160,20 @@ class SandboxTemplate(BaseModel): license: str = "" tags: list[str] = Field(default_factory=list) - spec_version: int + spec_version: int = Field(alias="specVersion") provides: dict[str, Any] start_cmd: str | None = None ready_cmd: str | None = None server: dict[str, Any] | None = None - """Declarative SandboxServer policy (the ``server:`` block). ``None`` keeps the server's - default behaviour. Compiled to ``server-policy.json`` by the toolchain.""" + """No v1 resource-level policy is inferred; configure the actual server separately.""" - model_config = {"extra": "allow"} + model_config = ConfigDict(extra="allow", populate_by_name=True) @model_validator(mode="before") @classmethod def _reject_removed_keys(cls, data: Any) -> Any: - from agent_sandbox.workflows.launch import validate_template_manifest + from agent_sandbox.template_contract import normalize_document if isinstance(data, dict) and any( k in data @@ -190,8 +188,21 @@ def _reject_removed_keys(cls, data: Any) -> Any: raise ValueError( "retired template fields are not accepted; use Dockerfile and template startup" ) - validate_template_manifest(data) - return data + intent = normalize_document(data) + sb = intent.get("sandbox", {}) + instance = sb.get("instance", {}) + services = sb.get("services", {}) + props = sb.get("template", {}).get("props", {}) + return { + **intent, + "env": instance.get("envs", {}), + "provides": services, + "ports": sorted(set(sb.get("ports", [])) | {v["port"] for v in services.values()}), + "cpu_count": props.get("cpu") if type(props.get("cpu")) is int else None, + "memory_mb": props.get("memory") if type(props.get("memory")) is int else None, + "start_cmd": sb.get("start"), + "ready_cmd": sb.get("ready"), + } @field_validator("server") @classmethod @@ -200,7 +211,7 @@ def _validate_server(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: def discovery_capabilities(self) -> set[str]: """Describe exposed ports without claiming platform execution capabilities.""" - return {"ports"} if self.provides else set() + return {"ports"} if self.ports else set() class TemplateRef(BaseModel): diff --git a/client-sdk/src/agent_sandbox/providers/alicloud_express/managed.py b/client-sdk/src/agent_sandbox/providers/alicloud_express/managed.py index 65206b61..f7e1c22f 100644 --- a/client-sdk/src/agent_sandbox/providers/alicloud_express/managed.py +++ b/client-sdk/src/agent_sandbox/providers/alicloud_express/managed.py @@ -15,7 +15,13 @@ class ExpressBackend: """Create only owned Express sandboxes using fully resolved service configuration.""" - def __init__(self, provider: dict[str, Any], model: dict[str, Any] | None = None) -> None: + def __init__( + self, + provider: dict[str, Any], + model: dict[str, Any] | None = None, + *, + instance_env: dict[str, str] | None = None, + ) -> None: if ( set(provider) != {"provider", "apiKey", "baseUrl", "domain", "region"} or provider["provider"] != "alicloud-express" @@ -46,6 +52,7 @@ def __init__(self, provider: dict[str, Any], model: dict[str, Any] | None = None "Service endpoints must be explicit HTTPS URLs without embedded credentials" ) self.provider, self.model = dict(provider), dict(model) if model else None + self.instance_env = dict(instance_env or {}) async def _infra(self) -> Any: from agent_sandbox.providers.alicloud_express.provider import AlicloudExpressProvider @@ -92,18 +99,32 @@ async def create( raise ManagedError( 400, "model_env_not_allowed", "Required model variables are not allowed" ) + from agent_sandbox.models.managed_instance import validate_instance, worker_environment + + instance = validate_instance( + settings.get("instance", {}), + settings["workspace"], + settings["sessions"]["maxLifetimeSeconds"], + ) + if instance["timeout"] != settings["sessions"]["maxLifetimeSeconds"]: + raise ValueError("Session lifetime does not match its instance snapshot") + custom_env = worker_environment(self.instance_env, settings.get("instanceEnvNames", [])) + instance.pop("lifecycle", None) # Fixed kill/no-resume policy, checked before I/O. + custom_metadata = instance.pop("metadata") infra = await self._infra() try: config = SandboxConfig( templateID=settings["template"], - timeout=settings["sessions"]["maxLifetimeSeconds"], + **instance, secure=True, metadata={ + **custom_metadata, "abx-managed": "true", "abx-session": session["id"], "abx-revision": session["_revision"], }, envVars={ + **custom_env, "ABX_SERVER_TOKEN": token, "ABX_MANAGED_WORKSPACE": settings["workspace"], "OPENAI_API_KEY": self.model["apiKey"], diff --git a/client-sdk/src/agent_sandbox/providers/base.py b/client-sdk/src/agent_sandbox/providers/base.py index a6faf7f7..53ac69bc 100644 --- a/client-sdk/src/agent_sandbox/providers/base.py +++ b/client-sdk/src/agent_sandbox/providers/base.py @@ -227,6 +227,16 @@ class Provider(ABC): #: Optional create inputs explicitly implemented by the backend. create_options: frozenset[str] = frozenset() + def validate_template_input(self, stage: str, value: Any) -> bool | None: + """Optionally validate v1 input using this Provider version's real schema. + + Override for ``template.prepare`` (the template object) and + ``sandbox.create`` (the instance object). Validation must be synchronous; + raise or return False to reject. The default supplies no v1 validator. + This method does not grant operations or certify backend admission. + """ + raise ProviderFeatureNotSupportedError(self.id, "template.v1-validation") + def session_protocols(self, http: Any) -> dict[str, Any]: """Missing protocols fail explicitly rather than acquiring a native transport.""" diff --git a/client-sdk/src/agent_sandbox/template_contract/__init__.py b/client-sdk/src/agent_sandbox/template_contract/__init__.py new file mode 100644 index 00000000..3e982078 --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/__init__.py @@ -0,0 +1,74 @@ +"""SDK admission for the immutable, template-owner maintained v1 contract. + +This module validates literals. Native Devs expressions require the Devs parser; +the Python SDK never substitutes environment values or implements another DSL. +Compiled documents may contain secrets and must remain private in memory. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from jsonschema import Draft202012Validator + +from .normalize import ( + MAX_YAML_BYTES, + compile_template_v1, + normalize_template_v1, + parse_yaml_v1, + schema_for_stage, +) + +if TYPE_CHECKING: + from collections.abc import Callable + +_SCHEMA = json.loads(Path(__file__).with_name("template-v1.schema.json").read_text()) +_VALIDATORS = { + stage: Draft202012Validator(schema_for_stage(_SCHEMA, stage)).validate + for stage in ("intent", "resolved") +} + + +def _read_yaml_document(path: str | Path) -> tuple[bytes, Any]: + """Read bounded bytes and apply the shared Core1.2 admission before any resource I/O.""" + with Path(path).open("rb") as source: + raw = source.read(MAX_YAML_BYTES + 1) + return raw, parse_yaml_v1(raw) + + +def normalize_document(manifest: dict[str, Any], *, provider: str | None = None) -> dict[str, Any]: + """Admit public v1 intent without evaluating expressions or resolving secrets.""" + return normalize_template_v1(manifest, _VALIDATORS["intent"], provider=provider) + + +def compile_literal_document( + manifest: dict[str, Any], + *, + provider: str, + validate_provider: Callable[[str, Any, str], Any] | None = None, +) -> dict[str, Any]: + """Compile a literal or Devs-resolved v1 snapshot, rejecting unresolved input.""" + + def reject_expressions(value: Any) -> None: + if isinstance(value, str) and "${" in value: + raise ValueError( + "Template v1: unresolved native expressions require the Devs parser; " + "pass its resolved v1 snapshot or literal values" + ) + if isinstance(value, dict): + for key, item in value.items(): + reject_expressions(key) + reject_expressions(item) + elif isinstance(value, list): + for item in value: + reject_expressions(item) + + reject_expressions(manifest) + return compile_template_v1( + manifest, _VALIDATORS["resolved"], provider=provider, validate_provider=validate_provider + ) + + +__all__ = ["normalize_document", "compile_literal_document"] diff --git a/client-sdk/src/agent_sandbox/template_contract/contract.json b/client-sdk/src/agent_sandbox/template_contract/contract.json new file mode 100644 index 00000000..cc421f5a --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/contract.json @@ -0,0 +1,209 @@ +{ + "version": 1, + "activation": "inactive-pending-consumer-migration", + "providerInputs": { + "alicloud-express": { + "prepareSchema": "project-prepare/1", + "createSchema": "high-level-create/1", + "sourceRevision": "16e0a2bfe50ac89b1d6c8ee21e3bcc995cd12614" + }, + "opensandbox": { + "prepareSchema": "project-prepare/1", + "createSchema": "high-level-create/1", + "sourceRevision": "ec6226e3294e29d1188855913a1e45548a674602" + }, + "e2b": { + "prepareSchema": "project-prepare/1", + "createSchema": "high-level-create/1", + "sourceRevision": "ec6226e3294e29d1188855913a1e45548a674602" + }, + "cubesandbox": { + "prepareSchema": "project-prepare/1", + "createSchema": "high-level-create/1", + "sourceRevision": "ec6226e3294e29d1188855913a1e45548a674602" + }, + "alicloud-container": { + "prepareSchema": "project-prepare/1", + "createSchema": "high-level-create/1", + "sourceRevision": "ec6226e3294e29d1188855913a1e45548a674602" + } + }, + "templateDefaults": { + "cpu": 2, + "memory": 2048 + }, + "instanceDefaults": { + "timeout": 300, + "envs": {}, + "metadata": {} + }, + "managedDefaults": { + "sessions": { + "idleTimeoutSeconds": 900, + "maxLogicalSessionsPerOwner": 100, + "maxActiveSessions": 20, + "maxQueuedTurnsPerSession": 8, + "resultRetentionSeconds": 86400 + }, + "limits": { + "maxRunSeconds": 600, + "maxToolSeconds": 60, + "maxConcurrentRuns": 10, + "maxQueuedRuns": 80, + "maxRunsPerMinute": 60, + "maxEventsPerRun": 10000, + "maxOutputBytes": 8388608, + "maxSubscribersPerSession": 4 + }, + "storage": { + "maxFileBytes": 10485760, + "maxArtifactBytes": 10485760, + "maxTurnArtifactBytes": 10485760, + "maxRetainedBytesPerOwner": 104857600 + }, + "bindings": { + "clientAuth": { + "generate": true, + "owner": "default" + }, + "internalAuth": { + "generate": true + }, + "ingress": { + "aliasName": "prod" + } + } + }, + "managedProfile": { + "defaults": { + "api": { + "protocol": "agent-sandbox-managed-agents/v1", + "compatibility": "openai-agents-inspired-subset", + "busyInputPolicy": "reject-unless-steerable", + "explicitQueue": true + }, + "runtime": { + "placement": "in-sandbox", + "driver": "sandbox-local-api", + "profile": "managed-bridge-v1" + }, + "gateway": { + "placement": "fc-web" + }, + "sandbox": { + "lifetime": "per-session", + "workspace": "exclusive", + "requiredFeatures": [ + "sandbox.lifecycle", + "ports.url" + ] + }, + "sessions": { + "maxConcurrentTurnsPerSession": 1, + "closePolicy": "release-compute-retain-results", + "deletePolicy": "remove-public-data-retain-private-cleanup", + "guestRestartPolicy": "freeze-and-reconcile" + }, + "state": { + "profile": "durable" + }, + "storage": { + "backend": "bounded-database" + }, + "deployment": { + "revisionStrategy": "immutable-workers-with-drain" + }, + "outputs": { + "endpoint": { + "from": "publish.outputs.endpoint" + }, + "protocol": "agent-sandbox-managed-agents/v1" + } + }, + "bindings": { + "state": { + "targets": [ + "gateway", + "worker", + "dispatcher" + ] + }, + "sandboxAccess": { + "targets": [ + "worker", + "dispatcher" + ] + }, + "model": { + "targets": [ + "sandbox-creation" + ], + "resolverRole": "worker", + "allowedEnv": [ + "OPENAI_API_KEY", + "OPENAI_BASE_URL", + "OPENAI_MODEL" + ] + }, + "clientAuth": { + "targets": [ + "gateway" + ] + }, + "internalAuth": { + "targets": [ + "gateway", + "worker", + "dispatcher" + ] + }, + "ingress": { + "targets": [ + "deployment-publisher" + ] + } + } + }, + "managedSandbox": { + "start": "python -m agent_sandbox.server.managed --port 9000", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-managed-bridge", + "version": 1, + "port": 9000, + "operations": [ + "health", + "capabilities", + "session.create", + "session.get", + "submissions.create", + "submissions.get", + "submissions.cancel" + ] + } + } + }, + "protectedManagedProps": [ + "functionName", + "description", + "environmentVariables.ABX_MANAGED_CONFIG_JSON", + "environmentVariables.ABX_MANAGED_DATABASE_URL", + "environmentVariables.ABX_MANAGED_INTERNAL_TOKEN", + "environmentVariables.ABX_MANAGED_FC_ENDPOINT", + "environmentVariables.ABX_MANAGED_AUTH_JSON", + "environmentVariables.ABX_MANAGED_PROVIDER_CONFIG_JSON", + "environmentVariables.ABX_MANAGED_MODEL_CONFIG_JSON", + "customContainerConfig.image", + "customContainerConfig.port", + "customContainerConfig.entrypoint", + "customContainerConfig.command", + "environmentVariables.ABX_MANAGED_SANDBOX_ENV_JSON", + "environmentVariables.ABX_MANAGED_SANDBOX_ENV_FILE" + ], + "managedInstanceDefaults": { + "timeout": 3600, + "envs": {}, + "metadata": {} + } +} diff --git a/client-sdk/src/agent_sandbox/template_contract/fixtures.json b/client-sdk/src/agent_sandbox/template_contract/fixtures.json new file mode 100644 index 00000000..633a87eb --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/fixtures.json @@ -0,0 +1,4603 @@ +{ + "version": 1, + "validTemplates": [ + { + "id": "express-reference-volume", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "unbound-source-portable", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "source-context-express", + "stage": "resolved", + "provider": "alicloud-express", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "source-context-opensandbox", + "stage": "resolved", + "provider": "opensandbox", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "ordinary-fc-domain-expression", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "ordinary-api", + "deployment": { + "edition": "3.0.0", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "api": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "functionName": "ordinary-api", + "runtime": "custom-container", + "memorySize": "${env('FC_MEMORY')}", + "environmentVariables": { + "MODEL_API_KEY": "${env('MODEL_API_KEY')}" + }, + "customContainerConfig": { + "image": "example.invalid/image:1", + "port": 9000 + } + } + }, + "domain": { + "component": "fc3-domain@0.0.29", + "props": { + "region": "${vars.region}", + "domainName": "api.example.invalid", + "protocol": "HTTPS", + "routeConfig": { + "routes": [ + { + "functionName": "${resources.api.output.functionName}", + "path": "/*" + } + ] + } + } + } + } + } + } + }, + { + "id": "managed-intent", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-resolved-native-memory", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "e2b-reference", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "e2b", + "template": { + "reference": "native-reference" + }, + "instance": { + "timeout": 300, + "envs": { + "MESSAGE": "hello" + } + } + } + } + }, + { + "id": "cubesandbox-reference", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "cubesandbox", + "template": { + "reference": "native-reference" + }, + "instance": { + "timeout": 300, + "envs": { + "MESSAGE": "hello" + } + } + } + } + }, + { + "id": "alicloud-container-reference", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-container", + "template": { + "reference": "native-reference" + }, + "instance": { + "timeout": 300, + "envs": { + "MESSAGE": "hello" + } + } + } + } + }, + { + "id": "opensandbox-image-options", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "opensandbox", + "template": { + "image": "example.invalid/image:1" + }, + "instance": { + "gpu": "1", + "secure": true, + "provider_options": { + "network_policy": "{\"defaultAction\":\"deny\",\"egress\":[]}", + "entrypoint": "python3 commands.py" + } + } + } + } + }, + { + "id": "third-party-descriptor", + "stage": "resolved", + "descriptor": true, + "compile": true, + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "third-party", + "template": { + "reference": "owned-reference" + }, + "instance": { + "timeout": 300 + } + } + } + }, + { + "id": "managed-budget-native-expression", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "limits": { + "maxRunSeconds": "${env('TURN_LIMIT')}" + } + } + } + }, + { + "id": "managed-explicit-auth-preserved", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + }, + "clientAuth": { + "tokens": [ + { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "owner": "custom", + "scopes": [ + "agents:read", + "agents:write" + ] + } + ] + }, + "internalAuth": { + "token": "fixture-internal-token-at-least-thirty-two-characters" + } + } + } + } + }, + { + "id": "managed-native-role-authority", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/fixture" + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/fixture" + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/fixture" + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-single-ttl", + "stage": "resolved", + "compile": true, + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + }, + "instance": { + "timeout": 1800 + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "independent-sandbox-and-native-serving", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "mixed", + "sandbox": { + "template": { + "reference": "owned" + }, + "provider": "alicloud-express" + }, + "deployment": { + "edition": "3.0.0", + "resources": { + "api": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou" + } + } + } + } + }, + "compile": true + }, + { + "id": "intent-descriptor-deferred", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": "${env('LIFETIME')}", + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + }, + "descriptor": true + }, + { + "id": "additional-access-ports-only", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + 9222, + 6080 + ] + } + }, + "compile": true + }, + { + "id": "service-and-additional-port-union", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + 9222, + 9000 + ], + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [] + } + } + } + }, + "compile": true + }, + { + "id": "long-native-reference-unrestricted", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "native/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + }, + "compile": true + } + ], + "invalidTemplates": [ + { + "id": "old-public-version", + "stage": "intent", + "manifest": { + "name": "example", + "spec_version": 5, + "provides": {} + } + }, + { + "id": "public-parameters-rejected", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + }, + "parameters": {} + } + }, + { + "id": "source-and-reference", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "source": {}, + "reference": "existing" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "no-template-input", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": {}, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "unbound-resolved-provider", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "provider-context-conflict", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + }, + "provider": "opensandbox" + }, + { + "id": "third-party-no-descriptor", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "third-party", + "template": { + "reference": "owned-reference" + }, + "instance": { + "timeout": 300 + } + } + } + }, + { + "id": "template-props-existing-reference", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template", + "props": { + "cpu": 2 + } + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "foreign-create-field", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + }, + "runtimeConfig": {} + } + } + } + }, + { + "id": "create-callback", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + }, + "on_created": "not-a-yaml-callback" + } + } + } + }, + { + "id": "wrong-create-dialect", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + }, + "memorySize": 1024 + } + } + } + }, + { + "id": "opensandbox-volume-rejected", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "opensandbox", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "opensandbox-gpu-device-name", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "opensandbox", + "template": { + "image": "example.invalid/image:1" + }, + "instance": { + "gpu": "A10", + "secure": true, + "provider_options": { + "network_policy": "{\"defaultAction\":\"deny\",\"egress\":[]}", + "entrypoint": "python3 commands.py" + } + } + } + } + }, + { + "id": "e2b-unconfirmed-memory", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "e2b", + "template": { + "reference": "native-reference" + }, + "instance": { + "timeout": 300, + "envs": { + "MESSAGE": "hello" + }, + "memory": 1024 + } + } + } + }, + { + "id": "unsafe-volume-root", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/var/data": "existing" + } + } + } + } + }, + { + "id": "unsafe-volume-hidden", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data/.hidden": "existing" + } + } + } + } + }, + { + "id": "ttl-too-large", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 86401, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "invalid-lifecycle", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + }, + "lifecycle": { + "onTimeout": "kill", + "autoResume": true + } + } + } + } + }, + { + "id": "memory-unit-multiple", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2000 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + }, + "provider": "alicloud-express" + }, + { + "id": "memory-cpu-ratio", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 1024 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + }, + "provider": "alicloud-express" + }, + { + "id": "recipe-traversal", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "../Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "alternate-recipe-traversal", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "../Dockerfile", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ] + } + } + } + } + }, + { + "id": "service-extra", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "python-hello", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable": { + "dockerfile": "Dockerfile.portable", + "context": "." + } + } + }, + "props": { + "cpu": 2, + "memory": 2048 + }, + "aliases": [ + "hello" + ] + }, + "instance": { + "envs": { + "ABX_SERVER_TOKEN": "", + "APP_MESSAGE": "hello" + } + }, + "start": "python commands.py", + "ready": "agent-sandbox-server healthcheck --port 9000", + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.list", + "commands.run" + ], + "label": "unexpected" + } + } + } + } + }, + { + "id": "managed-missing-role", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "missing", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-reused-role", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "publicApi", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-native-identity-conflict", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "functionName": "chosen-name" + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-private-env-conflict", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "environmentVariables": { + "ABX_MANAGED_INTERNAL_TOKEN": "do-not-accept" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-native-entrypoint-conflict", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "customContainerConfig": { + "entrypoint": [ + "python", + "other" + ] + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-plaintext-secret", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "do-not-store-plaintext", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-other-provider", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + }, + "provider": "opensandbox" + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-budget-order", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "limits": { + "maxToolSeconds": 700 + } + } + } + }, + { + "id": "managed-region-conflict", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-shanghai", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-sandbox-region-conflict", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-shanghai" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "expression-not-resolved", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": "${env('TTL')}", + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "managed-role-conflict", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/fixture" + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/other" + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "role": "acs:ram::123456789:role/fixture" + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-runtime-conflict", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "runtime": "nodejs20" + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "managed-duplicate-lifetime", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "sessions": { + "maxLifetimeSeconds": 3600 + } + } + } + }, + { + "id": "managed-ttl-budget-conflict", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + }, + "instance": { + "timeout": 500 + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "cn-hangzhou", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "postgresql://placeholder@db.example.invalid/agents" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "fixture-sandbox-key", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "fixture-model-key", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "protected-worker-bootstrap-abx_managed_sandbox_env_json", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "environmentVariables": { + "ABX_MANAGED_SANDBOX_ENV_JSON": "${env('CUSTOM')}" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "protected-worker-bootstrap-abx_managed_sandbox_env_file", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + }, + "environmentVariables": { + "ABX_MANAGED_SANDBOX_ENV_FILE": "${env('CUSTOM')}" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + } + }, + { + "id": "invalid-additional-port-boolean", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + true + ] + } + } + }, + { + "id": "invalid-additional-port-zero", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + 0 + ] + } + } + }, + { + "id": "invalid-additional-port-out-of-range", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + 65536 + ] + } + } + }, + { + "id": "invalid-additional-port-duplicate", + "stage": "resolved", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "ports": [ + 9000, + 9000 + ] + } + } + }, + { + "id": "identifier-trailing-lf-name", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example\n", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-lf-provider", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express\n", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-lf-service-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "services": { + "app\n": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [] + } + } + } + } + }, + { + "id": "identifier-trailing-lf-resource-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + }, + "deployment": { + "edition": "3.0.0", + "resources": { + "api\n": { + "component": "fc3@0.1.26", + "props": {} + } + } + } + } + }, + { + "id": "identifier-trailing-lf-profile-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable\n": { + "dockerfile": "Dockerfile.portable" + } + } + } + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-lf-hook-id", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "hooks": { + "afterReady": [ + { + "id": "info\n", + "type": "serverless-devs", + "step": "gateway", + "command": "info", + "timeoutSeconds": 30 + } + ] + } + } + } + }, + { + "id": "identifier-trailing-lf-hook-env", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "hooks": { + "afterReady": [ + { + "id": "check", + "type": "custom", + "argv": [ + "python", + "smoke.py" + ], + "cwd": ".", + "timeoutSeconds": 30, + "envAllowlist": [ + "MODEL_KEY\n" + ] + } + ] + } + } + } + }, + { + "id": "identifier-trailing-crlf-name", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example\r\n", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-crlf-provider", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express\r\n", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-crlf-service-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + }, + "services": { + "app\r\n": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [] + } + } + } + } + }, + { + "id": "identifier-trailing-crlf-resource-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + }, + "deployment": { + "edition": "3.0.0", + "resources": { + "api\r\n": { + "component": "fc3@0.1.26", + "props": {} + } + } + } + } + }, + { + "id": "identifier-trailing-crlf-profile-key", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "example", + "sandbox": { + "provider": "alicloud-express", + "template": { + "source": { + "dockerfile": "Dockerfile", + "profiles": { + "portable\r\n": { + "dockerfile": "Dockerfile.portable" + } + } + } + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + }, + { + "id": "identifier-trailing-crlf-hook-id", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "hooks": { + "afterReady": [ + { + "id": "info\r\n", + "type": "serverless-devs", + "step": "gateway", + "command": "info", + "timeoutSeconds": 30 + } + ] + } + } + } + }, + { + "id": "identifier-trailing-crlf-hook-env", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + }, + "hooks": { + "afterReady": [ + { + "id": "check", + "type": "custom", + "argv": [ + "python", + "smoke.py" + ], + "cwd": ".", + "timeoutSeconds": 30, + "envAllowlist": [ + "MODEL_KEY\r\n" + ] + } + ] + } + } + } + }, + { + "id": "engineering-directory-is-not-a-resource-name", + "stage": "intent", + "manifest": { + "specVersion": 1, + "name": "_skeleton", + "sandbox": { + "provider": "alicloud-express", + "template": { + "reference": "existing-template" + }, + "instance": { + "timeout": 300, + "volume_mounts": { + "/data": "existing-volume-name" + } + } + } + } + } + ], + "registry": { + "template": { + "specVersion": 1, + "name": "agents-api", + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + }, + "props": { + "cpu": 2, + "memory": 4096 + } + } + }, + "deployment": { + "edition": "3.0.0", + "access": "default", + "vars": { + "region": "cn-hangzhou" + }, + "resources": { + "publicApi": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 1, + "memorySize": 1024, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "sessionWorker": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + }, + "scheduler": { + "component": "fc3@0.1.26", + "props": { + "region": "${vars.region}", + "cpu": 2, + "memorySize": 4096, + "logConfig": { + "logstore": "existing-logstore", + "project": "existing-log-project" + } + } + } + } + }, + "application": { + "type": "agents-api", + "roles": { + "gateway": "publicApi", + "worker": "sessionWorker", + "dispatcher": "scheduler" + }, + "server": { + "source": { + "dockerfile": "server/Dockerfile" + } + }, + "state": { + "databaseUrl": "${env('AGENTS_DATABASE_URL')}" + }, + "bindings": { + "sandboxAccess": { + "apiKey": "${env('SANDBOX_API_KEY')}", + "baseUrl": "https://sandbox.example.invalid", + "domain": "sandbox.example.invalid", + "region": "cn-hangzhou" + }, + "model": { + "apiKey": "${env('OPENAI_API_KEY')}", + "baseUrl": "https://models.example.invalid/v1", + "model": "fixture-model", + "adapter": "qwen-code" + }, + "ingress": { + "kind": "function-alias", + "allowTestDomain": true + } + } + } + }, + "valid": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + }, + "invalid": [ + { + "id": "stale-target", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/missing", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "duplicate-target", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "secret-env-mismatch", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "UNUSED_KEY" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "form-runtime-default", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + }, + "default": "other" + } + ] + } + }, + { + "id": "invalid-pointer", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/~2bad", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "registry-identifier-trailing-lf-name", + "metadata": { + "specVersion": 1, + "name": "agents-api\n", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "registry-identifier-trailing-lf-environmentVariable", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY\n" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "registry-identifier-trailing-crlf-name", + "metadata": { + "specVersion": 1, + "name": "agents-api\r\n", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + }, + { + "id": "registry-identifier-trailing-crlf-environmentVariable", + "metadata": { + "specVersion": 1, + "name": "agents-api", + "version": "0.1.0", + "description": { + "en": "Managed Agents API", + "zh": "托管 Agents API" + }, + "author": "alibaba-agent-sandbox", + "license": "Apache-2.0", + "tags": [ + "agents-api" + ], + "readme": { + "en": "README.md", + "zh": "README.zh-CN.md" + }, + "questions": [ + { + "target": "/application/bindings/model/apiKey", + "title": { + "en": "Model API key", + "zh": "模型 API Key" + }, + "sensitive": true, + "environmentVariable": "OPENAI_API_KEY\r\n" + }, + { + "target": "/deployment/access", + "title": { + "en": "Cloud access", + "zh": "云账号" + } + } + ] + } + } + ] + } +} diff --git a/client-sdk/src/agent_sandbox/template_contract/normalize.py b/client-sdk/src/agent_sandbox/template_contract/normalize.py new file mode 100644 index 00000000..23ce0018 --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/normalize.py @@ -0,0 +1,663 @@ +"""Pure v1 admission/defaulting; expressions are evaluated only by native Devs.""" + +from __future__ import annotations + +import inspect +import json +import math +import re +from collections.abc import Callable +from copy import deepcopy +from pathlib import Path +from typing import Any + +DEFAULT_CONTRACT = json.loads(Path(__file__).with_name("contract.json").read_text()) +Validator = Callable[[Any], Any] +MAX_YAML_BYTES = 1024 * 1024 + + +def _fail(path: str, message: str) -> None: + raise ValueError(f"Template v1 {path}: {message}") + + +def _expression(value: Any) -> bool: + return isinstance(value, str) and value.startswith("${") and value.endswith("}") + + +def _finite(value: Any, path: str = "/") -> None: + if value is not None and not isinstance(value, (str, bool, int, float, list, dict)): + _fail(path, "expected an interoperable JSON value") + if ( + isinstance(value, int) + and not isinstance(value, bool) + and abs(value) > 9007199254740991 + ) or ( + isinstance(value, float) + and ( + not math.isfinite(value) + or (value.is_integer() and abs(value) > 9007199254740991) + ) + ): + _fail(path, "expected a finite interoperable JSON number") + if isinstance(value, list): + for index, item in enumerate(value): + _finite(item, f"{path}/{index}") + elif isinstance(value, dict): + for key, item in value.items(): + if not isinstance(key, str): + _fail(path, "expected string JSON object keys") + _finite(item, f"{'' if path == '/' else path}/{key}") + + +def parse_yaml_v1(source: str | bytes) -> Any: + """Read bounded JSON-compatible YAML1.2 Core; no files, env or expression evaluation.""" + if not isinstance(source, (str, bytes)): + raise TypeError("Template v1 /: expected YAML UTF-8 text or bytes") + try: + data = source.encode("utf-8", "strict") if isinstance(source, str) else source + text = data.decode("utf-8", "strict") + except UnicodeError: + raise ValueError("Template v1 /: YAML must contain valid UTF-8") from None + if len(data) > MAX_YAML_BYTES: + _fail("/", "YAML exceeds 1 MiB") + if re.search(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", text): + _fail("/", "YAML contains unsupported control characters") + + # Library Core rules own scalar resolution; SafeLoader's YAML1.1 rules are + # never changed globally. The subclass enforces only this source policy. + from yaml.nodes import ScalarNode + from yaml.tokens import AliasToken, DirectiveToken + from yamlcore import CoreLoader + + class SourceLoader(CoreLoader): + def get_token(self): + token = super().get_token() + if isinstance(token, AliasToken): + raise TypeError("aliases are unsupported") + if isinstance(token, DirectiveToken) and ( + token.name not in {"YAML", "TAG"} + or (token.name == "YAML" and token.value != (1, 2)) + ): + raise ValueError("only YAML1.2 directives are supported") + return token + + def construct_object(self, node, deep=False): + if ( + isinstance(node, ScalarNode) + and node.tag + in { + "tag:yaml.org,2002:bool", + "tag:yaml.org,2002:int", + "tag:yaml.org,2002:float", + "tag:yaml.org,2002:null", + } + and self.resolve(ScalarNode, node.value, (True, False)) != node.tag + ): + raise ValueError("explicit scalar tags cannot change the Core kind") + return super().construct_object(node, deep=deep) + + loader = None + try: + loader = SourceLoader(text) + value = loader.get_single_data() + _finite(value) + # Also reject invalid Unicode produced by escaped lone surrogate values. + json.dumps(value, ensure_ascii=False, allow_nan=False).encode("utf-8", "strict") + return value + except Exception: # noqa: BLE001 - Parser errors contain source lines/values. + raise ValueError( + "Template v1 /: invalid JSON-compatible YAML1.2 Core: use one document " + "with string keys; duplicate keys, aliases, unsupported tags and unsafe " + "values are not allowed" + ) from None + finally: + if loader is not None: + loader.dispose() + + +def _checked(value: Any, validate: Validator) -> None: + if not callable(validate): + raise TypeError( + "Pass an actual schema validator for the selected validation stage" + ) + try: + result = validate(value) + if inspect.isawaitable(result): + if hasattr(result, "close"): + result.close() + _fail("/", "use a synchronous schema validator") + if result is False: + _fail("/", "schema validation failed") + except Exception as error: + if isinstance(error, ValueError) and str(error).startswith("Template v1 "): + raise error from None + path = "/" + "/".join(str(key) for key in getattr(error, "path", [])) + raise ValueError(f"Template v1 {path}: schema validation failed") from None + + +def _fill(target: dict[str, Any], values: dict[str, Any]) -> None: + for key, value in values.items(): + if key not in target: + target[key] = deepcopy(value) + elif isinstance(value, dict) and isinstance(target[key], dict): + _fill(target[key], value) + + +def _local_recipe(recipe: dict[str, Any], path: str) -> None: + _fill(recipe, {"dockerfile": "Dockerfile", "context": "."}) + if ( + recipe["dockerfile"].startswith("/") + or "\\" in recipe["dockerfile"] + or "\0" in recipe["dockerfile"] + or any(part in ("", ".", "..") for part in recipe["dockerfile"].split("/")) + ): + _fail(path + "/dockerfile", "expected a contained relative file path") + + +def _field_at(value: Any, path: str) -> tuple[bool, Any]: + for key in path.split("."): + if not isinstance(value, dict) or key not in value: + return False, None + value = value[key] + return True, value + + +def _numeric_order(parent: dict[str, Any], lower: str, upper: str, path: str) -> None: + if ( + isinstance(parent[lower], (int, float)) + and isinstance(parent[upper], (int, float)) + and parent[lower] > parent[upper] + ): + _fail(path + "/" + lower, f"must be at most {upper}") + + +def schema_for_stage(schema: dict[str, Any], stage: str = "intent") -> dict[str, Any]: + """Clone the intent schema for post-native-parser validation without evaluation.""" + if stage not in ("intent", "resolved"): + raise ValueError("Unknown template validation stage") + result = deepcopy(schema) + if stage == "resolved": + + def strip(value: Any) -> Any: + if isinstance(value, list): + return [strip(item) for item in value] + if not isinstance(value, dict): + return value + branches = value.get("oneOf") + if ( + isinstance(branches, list) + and branches + and branches[-1].get("$ref") == "#/$defs/expression" + ): + alternatives = [strip(item) for item in branches[:-1]] + return ( + alternatives[0] + if len(alternatives) == 1 + else {**value, "oneOf": alternatives} + ) + return {key: strip(item) for key, item in value.items()} + + result = strip(result) + result["$defs"]["secret"] = {"type": "string", "minLength": 1} + result["properties"]["sandbox"]["required"] = ["provider", "template"] + + return result + + +def normalize_template_v1( + manifest: dict[str, Any], + validate: Validator, + *, + stage: str = "intent", + contract: dict[str, Any] = DEFAULT_CONTRACT, + provider: str | None = None, + validate_provider: Callable[[str, Any, str], Any] | None = None, +) -> dict[str, Any]: + """Validate a v1 document, fill its sole defaults and check cross-field invariants.""" + if stage not in ("intent", "resolved"): + raise ValueError("Unknown template validation stage") + _finite(manifest) + result = deepcopy(manifest) + if isinstance(result, dict) and isinstance(result.get("sandbox"), dict): + declared = result["sandbox"].get("provider") + if provider and declared and provider != declared: + _fail("/sandbox/provider", "conflicts with the selected Provider context") + if not declared and provider: + result["sandbox"]["provider"] = provider + if ( + not result["sandbox"].get("provider") + and isinstance(result.get("application"), dict) + and result["application"].get("type") == "agents-api" + ): + result["sandbox"]["provider"] = "alicloud-express" + _checked(result, validate) + sb, app = result.get("sandbox"), result.get("application") + if sb: + if "source" in sb["template"]: + _local_recipe(sb["template"]["source"], "/sandbox/template/source") + for name, recipe in sb["template"]["source"].get("profiles", {}).items(): + _local_recipe(recipe, "/sandbox/template/source/profiles/" + name) + if sb["template"].get("reference") and sb["template"].get("props"): + _fail( + "/sandbox/template/props", + "preparation properties cannot modify an existing template reference", + ) + if not sb["template"].get("reference"): + sb["template"].setdefault("props", {}) + if sb.get("provider") in contract["providerInputs"]: + _fill( + sb["template"]["props"], + { + **contract["templateDefaults"], + **({"memory": 4096} if app else {}), + }, + ) + sb.setdefault("instance", {}) + _fill( + sb["instance"], + contract["managedInstanceDefaults"] + if app + else contract["instanceDefaults"], + ) + sb.setdefault("services", {}) + for service in sb["services"].values(): + service.setdefault("operations", []) + if ( + sb["instance"].get("lifecycle", {}).get("autoResume") is True + and sb["instance"]["lifecycle"]["onTimeout"] != "pause" + ): + _fail("/sandbox/instance/lifecycle/autoResume", "requires onTimeout=pause") + gpu = sb["instance"].get("gpu") + if ( + sb.get("provider") == "opensandbox" + and gpu + and not re.fullmatch(r"[0-9]+", gpu) + and not _expression(gpu) + ): + _fail( + "/sandbox/instance/gpu", + "this adapter accepts a numeric GPU count string", + ) + resources = sb["template"].get("props") + if ( + sb.get("provider") in contract["providerInputs"] + and resources + and isinstance(resources.get("memory"), (int, float)) + ): + memory, cpu = resources["memory"], resources.get("cpu") + if memory < 128 or memory % 64: + _fail( + "/sandbox/template/props/memory", + "preparation memory is at least 128 MB and a multiple of 64", + ) + if ( + sb.get("provider") == "alicloud-express" + and isinstance(cpu, (int, float)) + and (memory < cpu * 1024 or memory > cpu * 4096) + ): + _fail( + "/sandbox/template/props/memory", + "Express preparation uses 1024..4096 MB per vCPU", + ) + if ( + isinstance(sb["instance"]["timeout"], (int, float)) + and sb["instance"]["timeout"] > 86400 + ): + _fail( + "/sandbox/instance/timeout", + "exceeds the high-level API limit of 86400 seconds", + ) + if "deployment" in result: + _fill(result["deployment"], {"name": result["name"], "access": "default"}) + if app: + if sb["provider"] != "alicloud-express": + _fail( + "/sandbox/provider", + "the current Managed consumer supports alicloud-express only", + ) + app_defaults = deepcopy(contract["managedDefaults"]) + for auth in ("clientAuth", "internalAuth"): + if auth in app["bindings"]: + del app_defaults["bindings"][auth] + _fill(app, app_defaults) + if "source" in app["server"]: + _local_recipe(app["server"]["source"], "/application/server/source") + for key in ("start", "ready"): + if key in sb and sb[key] != contract["managedSandbox"][key]: + _fail("/sandbox/" + key, "conflicts with the Managed bridge runtime") + sb[key] = contract["managedSandbox"][key] + if sb["services"] and sb["services"] != contract["managedSandbox"]["services"]: + _fail( + "/sandbox/services", + "the Managed bridge service is fixed by this application type", + ) + sb["services"] = deepcopy(contract["managedSandbox"]["services"]) + selected = list(app["roles"].values()) + if len(set(selected)) != 3: + _fail( + "/application/roles", + "each Managed role needs a distinct declared resource", + ) + resource_names = set(result["deployment"]["resources"]) + for role, rid in app["roles"].items(): + if rid not in resource_names: + _fail("/application/roles/" + role, "resource reference was not found") + resource = result["deployment"]["resources"][rid] + if not re.fullmatch(r"fc3@[0-9]+\.[0-9]+\.[0-9]+", resource["component"]): + _fail( + "/deployment/resources/" + rid + "/component", + "the current Managed consumer requires a pinned fc3 component", + ) + if ( + isinstance(resource["props"], dict) + and resource["props"].get("runtime") is not None + and resource["props"]["runtime"] != "custom-container" + and not _expression(resource["props"]["runtime"]) + ): + _fail( + "/deployment/resources/" + rid + "/props/runtime", + "the Managed bridge requires custom-container", + ) + if isinstance(resource["props"], dict): + for path in contract["protectedManagedProps"]: + if _field_at(resource["props"], path)[0]: + _fail( + "/deployment/resources/" + + rid + + "/props/" + + path.replace(".", "/"), + "this value is generated by the Managed coordinator", + ) + components = [ + result["deployment"]["resources"][rid]["component"] for rid in selected + ] + if len(set(components)) != 1: + _fail( + "/application/roles", + "current Managed roles share one locked fc3 component version", + ) + if ( + isinstance(app["sessions"]["idleTimeoutSeconds"], (int, float)) + and isinstance(sb["instance"]["timeout"], (int, float)) + and app["sessions"]["idleTimeoutSeconds"] > sb["instance"]["timeout"] + ): + _fail( + "/application/sessions/idleTimeoutSeconds", + "must be at most sandbox.instance.timeout", + ) + _numeric_order( + app["limits"], "maxToolSeconds", "maxRunSeconds", "/application/limits" + ) + if ( + isinstance(app["limits"]["maxRunSeconds"], (int, float)) + and isinstance(sb["instance"]["timeout"], (int, float)) + and app["limits"]["maxRunSeconds"] >= sb["instance"]["timeout"] + ): + _fail( + "/application/limits/maxRunSeconds", + "must be less than sandbox.instance.timeout", + ) + _numeric_order( + app["storage"], + "maxArtifactBytes", + "maxTurnArtifactBytes", + "/application/storage", + ) + _numeric_order( + app["storage"], + "maxFileBytes", + "maxRetainedBytesPerOwner", + "/application/storage", + ) + _numeric_order( + app["storage"], + "maxTurnArtifactBytes", + "maxRetainedBytesPerOwner", + "/application/storage", + ) + if stage == "resolved": + regions = [ + result["deployment"]["resources"][rid]["props"].get("region") + for rid in selected + ] + if ( + any( + not isinstance(region, str) + or not re.fullmatch(r"cn-[a-z0-9-]+", region) + for region in regions + ) + or len(set(regions)) != 1 + ): + _fail( + "/application/roles", + "resolved Managed roles need the same supported literal region", + ) + role_arns = [ + result["deployment"]["resources"][rid]["props"]["role"] + for rid in selected + if "role" in result["deployment"]["resources"][rid]["props"] + ] + if role_arns and ( + any(not isinstance(value, str) or not value for value in role_arns) + or len(set(role_arns)) != 1 + ): + _fail( + "/application/roles", + "the current Managed consumer uses one function RAM role", + ) + if ( + role_arns + and app["bindings"]["internalAuth"].get("roleArn") + and role_arns[0] != app["bindings"]["internalAuth"]["roleArn"] + ): + _fail( + "/application/bindings/internalAuth/roleArn", + "conflicts with the declared native function role", + ) + if app["bindings"]["sandboxAccess"]["region"] != regions[0]: + _fail( + "/application/bindings/sandboxAccess/region", + "must match the resolved Managed FC region", + ) + _checked(result, validate) + if sb: + if ( + stage == "resolved" + and sb.get("provider") not in contract["providerInputs"] + and not callable(validate_provider) + ): + _fail( + "/sandbox/provider", + "the selected plugin must supply real preparation/create validators", + ) + if stage == "resolved" and validate_provider and sb.get("provider"): + + def invoke(stage: str, value: Any, path: str) -> None: + try: + accepted = validate_provider(stage, deepcopy(value), sb["provider"]) + if inspect.isawaitable(accepted): + if hasattr(accepted, "close"): + accepted.close() + _fail(path, "selected Provider validation failed") + if accepted is False: + _fail(path, "selected Provider validation failed") + except Exception: # noqa: BLE001 - Sanitize errors from external validators. + raise ValueError( + f"Template v1 {path}: selected Provider validation failed" + ) from None + + if not sb["template"].get("reference"): + invoke("template.prepare", sb["template"], "/sandbox/template") + invoke("sandbox.create", sb["instance"], "/sandbox/instance") + return result + + +def compile_template_v1( + manifest: dict[str, Any], + validate: Validator, + *, + stage: str = "resolved", + contract: dict[str, Any] = DEFAULT_CONTRACT, + provider: str | None = None, + validate_provider: Callable[[str, Any, str], Any] | None = None, +) -> dict[str, Any]: + """Compile only resolved v1 input to private fields; never emit old spec versions.""" + if stage != "resolved": + raise ValueError("Compile requires native-parser-resolved input") + intent = normalize_template_v1( + manifest, + validate, + stage=stage, + contract=contract, + provider=provider, + validate_provider=validate_provider, + ) + result: dict[str, Any] = { + "name": intent["name"], + "kind": "application" + if intent.get("application") + else "serving" + if intent.get("deployment") + else "sandbox", + } + if "deployment" in intent: + result["deployment"] = deepcopy(intent["deployment"]) + if "sandbox" in intent: + sb = intent["sandbox"] + result.update( + sandbox=deepcopy(sb), + provider=sb["provider"], + provides=deepcopy(sb["services"]), + env=deepcopy(sb["instance"]["envs"]), + ports=sorted( + set(sb.get("ports", [])) + | {service["port"] for service in sb["services"].values()} + ), + resources=deepcopy(sb["template"].get("props", {})), + start_cmd=sb.get("start"), + ready_cmd=sb.get("ready"), + ) + if sb["template"].get("aliases") is not None: + result["aliases"] = deepcopy(sb["template"]["aliases"]) + if sb["template"].get("source", {}).get("profiles") is not None: + result["build_profiles"] = deepcopy(sb["template"]["source"]["profiles"]) + if sb["template"].get("image"): + result["image"] = sb["template"]["image"] + if sb["template"].get("reference"): + result["template_id"] = sb["template"]["reference"] + if "application" in intent: + app, dep = intent["application"], intent["deployment"] + profile = deepcopy(contract["managedProfile"]) + gateway = dep["resources"][app["roles"]["gateway"]] + binding_values = {"state": deepcopy(app["state"]), **deepcopy(app["bindings"])} + binding_values["sandboxAccess"]["provider"] = intent["sandbox"]["provider"] + native_role = next( + ( + dep["resources"][rid]["props"]["role"] + for rid in app["roles"].values() + if "role" in dep["resources"][rid]["props"] + ), + None, + ) + if native_role is not None: + binding_values["internalAuth"]["roleArn"] = native_role + compiled = deepcopy(profile["defaults"]) + _fill( + compiled, + { + "sessions": app["sessions"], + "limits": app["limits"], + "storage": app["storage"], + }, + ) + compiled["sessions"] = { + **compiled["sessions"], + **deepcopy(app["sessions"]), + "maxLifetimeSeconds": intent["sandbox"]["instance"]["timeout"], + } + compiled["limits"] = deepcopy(app["limits"]) + compiled["storage"] = {**compiled["storage"], **deepcopy(app["storage"])} + compiled["sandbox"]["provider"] = intent["sandbox"]["provider"] + compiled["state"] = {**compiled["state"], "bindingRef": "state"} + compiled["bindings"] = { + name: {"reference": name, **deepcopy(options)} + for name, options in profile["bindings"].items() + } + compiled["deployment"] = { + **compiled["deployment"], + "profile": "fc-managed-agents", + "accessRef": dep["access"], + "region": gateway["props"]["region"], + "component": gateway["component"], + "ingressBindingRef": "ingress", + **( + {"serverBuildRecipe": deepcopy(app["server"]["source"])} + if "source" in app["server"] + else {"serverImage": app["server"]["image"]} + ), + } + if app.get("hooks"): + compiled["hooks"] = deepcopy(app["hooks"]) + result["application"] = compiled + result["bindingValues"] = binding_values + result["roleResources"] = deepcopy(app["roles"]) + result["roleProps"] = { + role: deepcopy(dep["resources"][rid]["props"]) + for role, rid in app["roles"].items() + } + return result + + +def pointer_value(document: Any, pointer: str) -> Any: + """Decode a non-root JSON Pointer without accepting stale/implicit paths.""" + if ( + not isinstance(pointer, str) + or not pointer.startswith("/") + or re.search(r"~(?![01])", pointer) + ): + _fail("/questions/target", "expected a non-root JSON Pointer") + value = document + for encoded in pointer[1:].split("/"): + key = encoded.replace("~1", "/").replace("~0", "~") + if isinstance(value, list): + if not re.fullmatch(r"0|[1-9][0-9]*", key) or int(key) >= len(value): + _fail("/questions/target", "target was not found") + value = value[int(key)] + else: + if not isinstance(value, dict) or key not in value: + _fail("/questions/target", "target was not found") + value = value[key] + return deepcopy(value) + + +def normalize_registry_v1( + metadata: dict[str, Any], + template: dict[str, Any], + validate_registry: Validator, + validate_template: Validator, + *, + contract: dict[str, Any] = DEFAULT_CONTRACT, +) -> dict[str, Any]: + """Validate optional metadata; runtime normalization never reads it.""" + _finite(metadata) + _checked(metadata, validate_registry) + result = deepcopy(metadata) + intent = normalize_template_v1(template, validate_template, contract=contract) + _fill(result, {"entry": "template.yaml", "questions": []}) + targets: set[str] = set() + for index, question in enumerate(result["questions"]): + if question["target"] in targets: + _fail(f"/questions/{index}/target", "duplicate question target") + targets.add(question["target"]) + value = pointer_value(intent, question["target"]) + if question.get("sensitive"): + env = question["environmentVariable"] + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", env) or value not in ( + "${env('" + env + "')}", + '${env("' + env + '")}', + ): + _fail( + f"/questions/{index}/target", + "a sensitive question must target its declared native env expression", + ) + return result diff --git a/client-sdk/src/agent_sandbox/template_contract/source.json b/client-sdk/src/agent_sandbox/template_contract/source.json new file mode 100644 index 00000000..af6fb690 --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/source.json @@ -0,0 +1,26 @@ +{ + "repository": "https://github.com/Alibaba-Agent-Sandbox/awesome-templates", + "revision": "ef695803cb78bf82b8473195fcb37b3e6357a363", + "files": { + "contracts/template-v1/normalize.py": { + "target": "normalize.py", + "sha256": "f38bfc6d81b41b215d32e0693ad5b87447a028545a04ca8527e5dca0c62656b0" + }, + "contracts/template-v1/contract.json": { + "target": "contract.json", + "sha256": "077e4dec79834ab9ffd21f4e1402c997b1b9bd412a201c6705b566e7a9ac39de" + }, + "contracts/template-v1/fixtures.json": { + "target": "fixtures.json", + "sha256": "9170a08abf20d16088cc5346940224d60ef2170e8a7a3dcfe97cd172ade7a863" + }, + "spec/template-v1.schema.json": { + "target": "template-v1.schema.json", + "sha256": "b08298cf699a5dd4e5ff8a9136922acfa41ea2e56c3aefde1a0c65fac84c8f7c" + }, + "contracts/template-v1/yaml-fixtures.json": { + "target": "yaml-fixtures.json", + "sha256": "dcd8560f1b7725a4a9b1900a52ac1fbd9a9d32fca9b12edd02fd7c132e56f283" + } + } +} diff --git a/client-sdk/src/agent_sandbox/template_contract/template-v1.schema.json b/client-sdk/src/agent_sandbox/template_contract/template-v1.schema.json new file mode 100644 index 00000000..8f6cf1de --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/template-v1.schema.json @@ -0,0 +1,1484 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/Alibaba-Agent-Sandbox/awesome-templates/main/spec/template-v1.schema.json", + "title": "Agent Sandbox template v1 intent contract (not enabled in active consumers)", + "type": "object", + "additionalProperties": false, + "properties": { + "specVersion": { + "const": 1 + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{0,62}(?![\\s\\S])" + }, + "sandbox": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "type": "string", + "pattern": "^[a-z][a-z0-9._-]*(?![\\s\\S])" + }, + "template": { + "type": "object", + "additionalProperties": false, + "properties": { + "source": { + "$ref": "#/$defs/source" + }, + "image": { + "type": "string", + "minLength": 1 + }, + "reference": { + "type": "string", + "minLength": 1 + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "props": { + "type": "object" + } + }, + "oneOf": [ + { + "required": [ + "source" + ], + "not": { + "anyOf": [ + { + "required": [ + "image" + ] + }, + { + "required": [ + "reference" + ] + } + ] + } + }, + { + "required": [ + "image" + ], + "not": { + "anyOf": [ + { + "required": [ + "source" + ] + }, + { + "required": [ + "reference" + ] + } + ] + } + }, + { + "required": [ + "reference" + ], + "not": { + "anyOf": [ + { + "required": [ + "source" + ] + }, + { + "required": [ + "image" + ] + } + ] + } + } + ] + }, + "instance": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "disk": { + "$ref": "#/$defs/nonnegativeIntegerInput" + }, + "gpu": { + "type": "string", + "minLength": 1 + }, + "secure": { + "$ref": "#/$defs/booleanInput" + }, + "volume_mounts": { + "type": "object", + "propertyNames": { + "pattern": "^/(?!bin(?:/|$)|opt(?:/|$)|var(?:/|$)|dev(?:/|$))(?!.*(?:^|/)\\.)(?!.*//)[^\\x00]+[^/]$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "lifecycle": { + "type": "object", + "additionalProperties": false, + "properties": { + "onTimeout": { + "enum": [ + "pause", + "kill" + ] + }, + "autoResume": { + "$ref": "#/$defs/booleanInput" + } + }, + "required": [ + "onTimeout" + ] + }, + "provider_options": { + "type": "object" + } + } + }, + "start": { + "type": "string", + "minLength": 1 + }, + "ready": { + "type": "string", + "minLength": 1 + }, + "services": { + "type": "object", + "propertyNames": { + "pattern": "^[a-z][a-z0-9_-]*(?![\\s\\S])" + }, + "additionalProperties": { + "$ref": "#/$defs/service" + } + }, + "ports": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + }, + "required": [ + "template" + ], + "allOf": [ + { + "if": { + "properties": { + "provider": { + "const": "alicloud-express" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "properties": { + "instance": { + "$ref": "#/$defs/instance_alicloud-express" + }, + "template": { + "properties": { + "props": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "opensandbox" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "properties": { + "instance": { + "$ref": "#/$defs/instance_opensandbox" + }, + "template": { + "properties": { + "props": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "e2b" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "properties": { + "instance": { + "$ref": "#/$defs/instance_e2b" + }, + "template": { + "properties": { + "props": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "cubesandbox" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "properties": { + "instance": { + "$ref": "#/$defs/instance_cubesandbox" + }, + "template": { + "properties": { + "props": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "alicloud-container" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "properties": { + "instance": { + "$ref": "#/$defs/instance_alicloud-container" + }, + "template": { + "properties": { + "props": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + } + } + } + } + } + } + } + } + ] + }, + "deployment": { + "type": "object", + "additionalProperties": false, + "properties": { + "edition": { + "const": "3.0.0" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "access": { + "type": "string", + "minLength": 1 + }, + "vars": { + "type": "object" + }, + "resources": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[A-Za-z][A-Za-z0-9_-]*(?![\\s\\S])" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "component": { + "type": "string", + "minLength": 1 + }, + "props": { + "$ref": "#/$defs/componentProps" + }, + "actions": {} + }, + "required": [ + "component", + "props" + ] + } + }, + "flow": { + "type": "object" + }, + "actions": {}, + "template": { + "type": "object" + }, + "validation": { + "type": "boolean" + } + }, + "required": [ + "edition", + "resources" + ], + "description": "Embedded native Serverless Devs 3 project. Native project semantics and component properties are validated by the actual selected Devs implementation, not this envelope schema." + }, + "application": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "agents-api" + }, + "roles": { + "type": "object", + "additionalProperties": false, + "properties": { + "gateway": { + "type": "string", + "minLength": 1 + }, + "worker": { + "type": "string", + "minLength": 1 + }, + "dispatcher": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "gateway", + "worker", + "dispatcher" + ] + }, + "server": { + "type": "object", + "additionalProperties": false, + "properties": { + "source": { + "$ref": "#/$defs/recipe" + }, + "image": { + "type": "string", + "pattern": "^[^\\s@]+@sha256:[0-9a-f]{64}$" + } + }, + "oneOf": [ + { + "required": [ + "source" + ], + "not": { + "required": [ + "image" + ] + } + }, + { + "required": [ + "image" + ], + "not": { + "required": [ + "source" + ] + } + } + ] + }, + "sessions": { + "type": "object", + "additionalProperties": false, + "properties": { + "idleTimeoutSeconds": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxLogicalSessionsPerOwner": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxActiveSessions": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxQueuedTurnsPerSession": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "resultRetentionSeconds": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + } + } + }, + "storage": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxFileBytes": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxArtifactBytes": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxTurnArtifactBytes": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxRetainedBytesPerOwner": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + } + } + }, + "limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxRunSeconds": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxToolSeconds": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxConcurrentRuns": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxQueuedRuns": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxRunsPerMinute": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxEventsPerRun": { + "oneOf": [ + { + "type": "integer", + "minimum": 5 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxOutputBytes": { + "oneOf": [ + { + "type": "integer", + "minimum": 1024 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "maxSubscribersPerSession": { + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + { + "$ref": "#/$defs/expression" + } + ] + } + } + }, + "state": { + "type": "object", + "additionalProperties": false, + "properties": { + "databaseUrl": { + "$ref": "#/$defs/secret" + }, + "vpcConfig": { + "type": "object" + }, + "databaseCa": { + "$ref": "#/$defs/secret" + } + }, + "required": [ + "databaseUrl" + ] + }, + "bindings": { + "type": "object", + "additionalProperties": false, + "properties": { + "sandboxAccess": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "$ref": "#/$defs/secret" + }, + "baseUrl": { + "type": "string", + "minLength": 1 + }, + "domain": { + "type": "string", + "minLength": 1 + }, + "region": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "apiKey", + "baseUrl", + "domain", + "region" + ] + }, + "model": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "$ref": "#/$defs/secret" + }, + "baseUrl": { + "type": "string", + "minLength": 1 + }, + "model": { + "type": "string", + "minLength": 1 + }, + "adapter": { + "enum": [ + "codex", + "qwen-code" + ] + } + }, + "required": [ + "apiKey", + "baseUrl", + "model", + "adapter" + ] + }, + "clientAuth": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "generate": { + "const": true, + "default": true + }, + "owner": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "generate" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "tokens": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "owner": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "agents:read", + "agents:write" + ] + } + }, + "expiresAt": { + "$ref": "#/$defs/positiveIntegerInput" + } + }, + "required": [ + "sha256", + "owner", + "scopes" + ] + } + } + }, + "required": [ + "tokens" + ] + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "internalAuth": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "generate": { + "const": true, + "default": true + }, + "roleArn": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "generate" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "token": { + "$ref": "#/$defs/secret" + }, + "roleArn": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "token" + ] + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "ingress": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "function-alias", + "custom-domain" + ] + }, + "aliasName": { + "type": "string", + "minLength": 1 + }, + "allowTestDomain": { + "type": "boolean" + }, + "domainName": { + "type": "string", + "minLength": 1 + }, + "protocol": { + "const": "HTTPS" + }, + "path": { + "const": "/" + } + }, + "required": [ + "kind" + ], + "allOf": [ + { + "if": { + "properties": { + "kind": { + "const": "function-alias" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "allowTestDomain" + ], + "properties": { + "allowTestDomain": { + "const": true + } + }, + "not": { + "anyOf": [ + { + "required": [ + "domainName" + ] + }, + { + "required": [ + "protocol" + ] + }, + { + "required": [ + "path" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "kind": { + "const": "custom-domain" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "domainName", + "protocol" + ], + "not": { + "required": [ + "allowTestDomain" + ] + } + } + } + ] + } + }, + "required": [ + "sandboxAccess", + "model", + "ingress" + ] + }, + "hooks": { + "type": "object", + "additionalProperties": false, + "properties": { + "beforeApply": { + "$ref": "#/$defs/hooks" + }, + "afterReady": { + "$ref": "#/$defs/hooks" + } + }, + "required": [] + } + }, + "required": [ + "type", + "roles", + "server", + "state", + "bindings" + ] + } + }, + "required": [ + "specVersion", + "name" + ], + "anyOf": [ + { + "required": [ + "sandbox" + ] + }, + { + "required": [ + "deployment" + ] + } + ], + "dependentRequired": { + "application": [ + "sandbox", + "deployment" + ] + }, + "$defs": { + "expression": { + "type": "string", + "pattern": "^\\$\\{[\\s\\S]+\\}$", + "description": "A native Serverless Devs expression; only the official parser evaluates it." + }, + "positiveIntegerInput": { + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "nonnegativeIntegerInput": { + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "booleanInput": { + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "secret": { + "type": "string", + "pattern": "^\\$\\{[\\s\\S]+\\}$", + "description": "A native Serverless Devs expression; only the official parser evaluates it." + }, + "componentProps": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/$defs/expression" + } + ] + }, + "source": { + "type": "object", + "additionalProperties": false, + "properties": { + "dockerfile": { + "type": "string", + "minLength": 1, + "default": "Dockerfile" + }, + "context": { + "const": ".", + "default": "." + }, + "profiles": { + "type": "object", + "propertyNames": { + "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*(?![\\s\\S])" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "dockerfile": { + "type": "string", + "minLength": 1, + "default": "Dockerfile" + }, + "context": { + "const": ".", + "default": "." + } + } + } + } + } + }, + "recipe": { + "type": "object", + "additionalProperties": false, + "properties": { + "dockerfile": { + "type": "string", + "minLength": 1, + "default": "Dockerfile" + }, + "context": { + "const": ".", + "default": "." + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "protocol": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "operations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "default": [] + } + }, + "required": [ + "protocol", + "version", + "port" + ] + }, + "openSandboxOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "entrypoint": { + "type": "string", + "minLength": 1 + }, + "ready_timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "network_policy": { + "type": "string", + "minLength": 1 + }, + "platform": { + "enum": [ + "linux/amd64", + "linux/arm64", + "windows/amd64", + "windows/arm64" + ] + }, + "secure_access": { + "$ref": "#/$defs/booleanInput" + } + } + }, + "instance_alicloud-express": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "disk": { + "$ref": "#/$defs/nonnegativeIntegerInput" + }, + "gpu": { + "type": "string", + "minLength": 1 + }, + "volume_mounts": { + "type": "object", + "propertyNames": { + "pattern": "^/(?!bin(?:/|$)|opt(?:/|$)|var(?:/|$)|dev(?:/|$))(?!.*(?:^|/)\\.)(?!.*//)[^\\x00]+[^/]$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "lifecycle": { + "type": "object", + "additionalProperties": false, + "properties": { + "onTimeout": { + "enum": [ + "pause", + "kill" + ] + }, + "autoResume": { + "$ref": "#/$defs/booleanInput" + } + }, + "required": [ + "onTimeout" + ] + } + } + }, + "instance_opensandbox": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "cpu": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "memory": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "gpu": { + "type": "string", + "minLength": 1 + }, + "secure": { + "$ref": "#/$defs/booleanInput" + }, + "provider_options": { + "$ref": "#/$defs/openSandboxOptions" + } + } + }, + "instance_e2b": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "instance_cubesandbox": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "instance_alicloud-container": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout": { + "$ref": "#/$defs/positiveIntegerInput" + }, + "envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "refName": { + "type": "string", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}(?![\\s\\S])" + }, + "relativePath": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\\\x00-\\x1f]+$" + }, + "hooks": { + "type": "array", + "maxItems": 8, + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/refName" + }, + "type": { + "const": "custom" + }, + "argv": { + "type": "array", + "minItems": 1, + "maxItems": 64, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "cwd": { + "$ref": "#/$defs/relativePath" + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 900 + }, + "envAllowlist": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*(?![\\s\\S])" + } + } + }, + "required": [ + "id", + "type", + "argv", + "cwd", + "timeoutSeconds", + "envAllowlist" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/refName" + }, + "type": { + "const": "serverless-devs" + }, + "step": { + "enum": [ + "gateway", + "worker", + "dispatcher" + ] + }, + "command": { + "const": "info" + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 900 + } + }, + "required": [ + "id", + "type", + "step", + "command", + "timeoutSeconds" + ] + } + ] + } + } + } +} diff --git a/client-sdk/src/agent_sandbox/template_contract/yaml-fixtures.json b/client-sdk/src/agent_sandbox/template_contract/yaml-fixtures.json new file mode 100644 index 00000000..213e2256 --- /dev/null +++ b/client-sdk/src/agent_sandbox/template_contract/yaml-fixtures.json @@ -0,0 +1,242 @@ +{ + "version": 1, + "valid": [ + { + "id": "core-string-values", + "text": "on: on\noff: OFF\ndate: 2026-11-12\nunderscores: 1_000\nbinary: 0b101\nhexUnderscores: 0x1_f\nsexagesimal: 1:20\n", + "expected": { + "on": "on", + "off": "OFF", + "date": "2026-11-12", + "underscores": "1_000", + "binary": "0b101", + "hexUnderscores": "0x1_f", + "sexagesimal": "1:20" + } + }, + { + "id": "core-numeric-values", + "text": "octal: 0o12\nhex: 0x12\nleadingZero: 010\nexponent: 1e2\ndecimal: 1.5\ninteger: 42\n", + "expected": { + "octal": 10, + "hex": 18, + "leadingZero": 10, + "exponent": 100, + "decimal": 1.5, + "integer": 42 + } + }, + { + "id": "core-bool-null", + "text": "truth: TRUE\nfalsity: false\nempty: null\ntilde: ~\nimplicit:\n", + "expected": { + "truth": true, + "falsity": false, + "empty": null, + "tilde": null, + "implicit": null + } + }, + { + "id": "quoted-values", + "text": "values: [\"true\", \"42\", \"2026-11-12\", \"null\"]\n", + "expected": { + "values": [ + "true", + "42", + "2026-11-12", + "null" + ] + } + }, + { + "id": "yaml12-directive", + "text": "%YAML 1.2\n---\nvalue: on\n", + "expected": { + "value": "on" + } + }, + { + "id": "multiline-unicode-native-expressions", + "text": "text: |\n hello 中文\n second line\nnative: \"${env('MODEL_KEY')}\"\n", + "expected": { + "text": "hello 中文\nsecond line\n", + "native": "${env('MODEL_KEY')}" + } + }, + { + "id": "literal-merge-key", + "text": "<<: {a: plain}\n", + "expected": { + "<<": { + "a": "plain" + } + } + }, + { + "id": "unused-anchor", + "text": "value: &unused on\n", + "expected": { + "value": "on" + } + }, + { + "id": "explicit-core-tags", + "text": "int: !!int 12\nfloat: !!float 1.0\nbool: !!bool true\n\"null\": !!null null\nstring: !!str true\n", + "expected": { + "int": 12, + "float": 1, + "bool": true, + "null": null, + "string": "true" + } + }, + { + "id": "json-subset", + "text": "{\"on\":\"on\",\"date\":\"2026-11-12\",\"items\":[1,true,null]}", + "expected": { + "on": "on", + "date": "2026-11-12", + "items": [ + 1, + true, + null + ] + } + }, + { + "id": "utf8-bom", + "text": "value: on\r\n", + "expected": { + "value": "on" + } + }, + { + "id": "template-env-scalars", + "text": "specVersion: 1\nname: example\nsandbox:\n template: {reference: native-id}\n instance:\n envs: {FLAG: on, WHEN: 2026-11-12}\n", + "expected": { + "specVersion": 1, + "name": "example", + "sandbox": { + "template": { + "reference": "native-id" + }, + "instance": { + "envs": { + "FLAG": "on", + "WHEN": "2026-11-12" + } + } + } + } + }, + { + "id": "empty-document", + "text": "", + "expected": null + } + ], + "invalid": [ + { + "id": "duplicate-key", + "text": "value: 1\nvalue: 2\n" + }, + { + "id": "duplicate-quoted-key", + "text": "\"value\": 1\nvalue: 2\n" + }, + { + "id": "alias", + "text": "first: &v on\nsecond: *v\n" + }, + { + "id": "recursive-alias", + "text": "loop: &v [*v]\n" + }, + { + "id": "unsupported-tag", + "text": "value: !custom private-source-canary\n" + }, + { + "id": "yaml11-tag", + "text": "value: !!timestamp 2026-11-12\n" + }, + { + "id": "yaml11-directive", + "text": "%YAML 1.1\n---\nvalue: on\n" + }, + { + "id": "yaml13-directive", + "text": "%YAML 1.3\n---\nvalue: on\n" + }, + { + "id": "unknown-directive", + "text": "%CUSTOM unknown\n---\nvalue: on\n" + }, + { + "id": "multi-doc", + "text": "---\na: 1\n---\nb: 2\n" + }, + { + "id": "numeric-key", + "text": "1: value\n" + }, + { + "id": "bool-key", + "text": "true: value\n" + }, + { + "id": "null-key", + "text": "null: value\n" + }, + { + "id": "complex-key", + "text": "? [one, two]\n: value\n" + }, + { + "id": "nonfinite-infinity", + "text": "value: .inf\n" + }, + { + "id": "nonfinite-nan", + "text": "value: .nan\n" + }, + { + "id": "unsafe-integer", + "text": "value: 9007199254740992\n" + }, + { + "id": "explicit-float-int-coercion", + "text": "value: !!float 1\n" + }, + { + "id": "explicit-bool-coercion", + "text": "value: !!bool on\n" + }, + { + "id": "raw-control-character", + "text": "value: \"\u0000\"\n" + }, + { + "id": "delete-control-character", + "text": "value: \"\"\n" + }, + { + "id": "escaped-lone-surrogate", + "text": "value: \"\\uD800\"\n" + }, + { + "id": "private-parser-error", + "text": "value: [private-source-canary\n" + }, + { + "id": "invalid-utf8", + "hex": "ff" + }, + { + "id": "over-byte-limit", + "repeat": "a", + "count": 1048577 + } + ] +} diff --git a/client-sdk/src/agent_sandbox/utils/registry.py b/client-sdk/src/agent_sandbox/utils/registry.py index 4f80f26b..21c8f6cf 100644 --- a/client-sdk/src/agent_sandbox/utils/registry.py +++ b/client-sdk/src/agent_sandbox/utils/registry.py @@ -8,7 +8,6 @@ from pathlib import Path import httpx -import yaml from agent_sandbox.models.errors import ( GitHubRateLimitError, @@ -17,6 +16,7 @@ TemplateNotFoundError, ) from agent_sandbox.models.template import SandboxTemplate, TemplateRef +from agent_sandbox.template_contract import _read_yaml_document from agent_sandbox.utils.github_token import ( FINE_GRAINED_PAT_URL, rate_limit_message, @@ -384,12 +384,5 @@ def clear_cache(self) -> int: def load_template_from_yaml(path: Path) -> SandboxTemplate: """从 YAML 文件加载模板定义。""" - with open(path) as f: - data = yaml.safe_load(f) - # 映射 resources 嵌套结构到扁平字段 - resources = data.get("resources", {}) or {} - if "application" not in data and "cpu" in resources and "cpu_count" not in data: - data["cpu_count"] = resources["cpu"] - if "application" not in data and "memory" in resources and "memory_mb" not in data: - data["memory_mb"] = resources["memory"] + _, data = _read_yaml_document(path) return SandboxTemplate.model_validate(data) diff --git a/client-sdk/src/agent_sandbox/workflows/launch-v1.json b/client-sdk/src/agent_sandbox/workflows/launch-v1.json index b7cbc579..63ee5669 100644 --- a/client-sdk/src/agent_sandbox/workflows/launch-v1.json +++ b/client-sdk/src/agent_sandbox/workflows/launch-v1.json @@ -1,10 +1,11 @@ { "version": 1, - "template_spec_version": 5, + "template_spec_version": 1, "max_safe_integer": 9007199254740991, "template_keys": [ - "spec_version", - "provides" + "specVersion", + "name", + "sandbox" ], "retired_keys": [ "workflow_contract_version", diff --git a/client-sdk/src/agent_sandbox/workflows/launch-vectors.json b/client-sdk/src/agent_sandbox/workflows/launch-vectors.json index 3c3e513a..d8d9c870 100644 --- a/client-sdk/src/agent_sandbox/workflows/launch-vectors.json +++ b/client-sdk/src/agent_sandbox/workflows/launch-vectors.json @@ -4,20 +4,27 @@ "id": "source-cli", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -78,20 +85,27 @@ "id": "source-python-unprepared", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -154,20 +168,27 @@ "id": "source-python-prepared", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -227,29 +248,36 @@ "id": "multiple-services", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } }, - "api": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9001, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + }, + "api": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9001, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -319,10 +347,17 @@ "id": "no-services", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": {}, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": {}, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -372,10 +407,15 @@ "id": "managed-no-start", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "provider": "fixture", - "template_id": "base" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "reference": "base" + }, + "services": {}, + "provider": "fixture" + } }, "provider": { "id": "fixture", @@ -423,10 +463,15 @@ "id": "managed-foreign-provider", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "provider": "other", - "template_id": "base" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "reference": "base" + }, + "services": {}, + "provider": "other" + } }, "provider": { "id": "fixture", @@ -475,19 +520,24 @@ "id": "image-entrypoint", "input": { "manifest": { - "spec_version": 5, - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "image": "registry/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } } - }, - "image": "registry/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } }, "provider": { "id": "fixture", @@ -546,10 +596,15 @@ "id": "ready-command", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "image": "image", - "ready_cmd": "test -f /tmp/ready" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "image": "image" + }, + "services": {}, + "ready": "test -f /tmp/ready" + } }, "provider": { "id": "fixture", @@ -598,20 +653,27 @@ "id": "unsupported-input", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -671,20 +733,27 @@ "id": "unsupported-startup", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -744,20 +813,27 @@ "id": "missing-port", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -814,20 +890,27 @@ "id": "retired-v4", "input": { "manifest": { - "spec_version": 4, + "specVersion": 4, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -867,20 +950,27 @@ "id": "retired-workflows", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" }, - "start_cmd": "python app.py", "workflows": {} }, "provider": { @@ -921,21 +1011,28 @@ "id": "unknown-service-field", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ], - "binding": {} - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ], + "binding": {} + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -975,21 +1072,24 @@ "id": "bad-provider-binding", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py", - "provider": "fixture" + "sandbox": { + "template": {}, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "provider": "fixture", + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -1029,20 +1129,27 @@ "id": "bad-port-bool", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": true, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": true, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", diff --git a/client-sdk/src/agent_sandbox/workflows/launch.py b/client-sdk/src/agent_sandbox/workflows/launch.py index 9af997b6..4cc1c5d1 100644 --- a/client-sdk/src/agent_sandbox/workflows/launch.py +++ b/client-sdk/src/agent_sandbox/workflows/launch.py @@ -2,53 +2,20 @@ from __future__ import annotations -import copy import json -import re from pathlib import Path from typing import Any +from agent_sandbox.template_contract import normalize_document + LAUNCH_CONTRACT: dict[str, Any] = json.loads( Path(__file__).with_name("launch-v1.json").read_text(encoding="utf-8") ) -_ID = re.compile(r"^[a-z][a-z0-9_-]*$") def validate_template_manifest(manifest: dict[str, Any]) -> dict[str, Any]: - """Validate current launch metadata without inventing application defaults.""" - if not isinstance(manifest, dict): - raise ValueError("template: expected an object") - if manifest.get("spec_version") != 5 or type(manifest.get("spec_version")) is not int: - raise ValueError("template.spec_version: expected 5; migrate retired workflow declarations") - if any(key in manifest for key in LAUNCH_CONTRACT["retired_keys"]): - raise ValueError("template: remove retired workflow and capability declarations") - services = manifest.get("provides") - if not isinstance(services, dict): - raise ValueError("template.provides: expected an object") - for name, service in services.items(): - if not isinstance(name, str) or not _ID.fullmatch(name): - raise ValueError("provides: invalid service ID") - if not isinstance(service, dict) or set(service) != set(LAUNCH_CONTRACT["service_keys"]): - raise ValueError("service: unknown or missing fields") - if not isinstance(service["protocol"], str) or not service["protocol"]: - raise ValueError("service.protocol: expected a nonempty string") - for field, maximum in [("version", LAUNCH_CONTRACT["max_safe_integer"]), ("port", 65535)]: - value = service[field] - if type(value) is not int or not 1 <= value <= maximum: - raise ValueError(f"service.{field}: expected a bounded integer") - ops = service["operations"] - if not isinstance(ops, list) or any(not isinstance(op, str) or not op for op in ops): - raise ValueError("service.operations: expected nonempty string entries") - if len(ops) != len(set(ops)): - raise ValueError("service.operations: duplicate entries") - for key in ("start_cmd", "ready_cmd", "image", "provider", "template_id"): - if key in manifest and (not isinstance(manifest[key], str) or not manifest[key].strip()): - raise ValueError(f"template.{key}: expected a nonempty string") - if ("provider" in manifest) != ("template_id" in manifest): - raise ValueError("template: provider and template_id must be declared together") - if "image" in manifest and "template_id" in manifest: - raise ValueError("template: image and template_id are mutually exclusive") - return copy.deepcopy(manifest) + """Validate canonical public v1 intent with owner-defined defaults.""" + return normalize_document(manifest) def plan_template_launch( @@ -74,30 +41,36 @@ def plan_template_launch( if not isinstance(operations, list) or any(not isinstance(op, str) for op in operations): raise ValueError("provider.operations: expected consumer operation declarations") reason = ( - "provider-mismatch" if body.get("provider") and body["provider"] != provider["id"] else None + "provider-mismatch" + if body.get("sandbox", {}).get("provider") and body["sandbox"]["provider"] != provider["id"] + else None + ) + sandbox = body.get("sandbox", {}) + template = sandbox.get("template", {}) + input_kind = ( + "managed" if "reference" in template else "image" if "image" in template else "source" ) - input_kind = "managed" if "template_id" in body else "image" if "image" in body else "source" if not (prepared and input_kind == "source") and input_kind not in provider.get( "workflow_inputs", [] ): reason = reason or "input-unimplemented" startup = ( - "exec" if body.get("start_cmd") else "entrypoint" if input_kind != "managed" else "none" + "exec" if sandbox.get("start") else "entrypoint" if input_kind != "managed" else "none" ) if startup not in provider.get("startup_modes", []): reason = reason or "startup-unimplemented" required = set(LAUNCH_CONTRACT["required_operations"]["create"]) if input_kind == "source" and not prepared: required.update(LAUNCH_CONTRACT["required_operations"]["prepare"]) - if body.get("start_cmd") or body.get("ready_cmd"): + if sandbox.get("start") or sandbox.get("ready"): required.update(LAUNCH_CONTRACT["required_operations"]["start"]) - if body["provides"]: + if sandbox.get("services", {}) or sandbox.get("ports", []): required.update(LAUNCH_CONTRACT["required_operations"]["services"]) missing = required - set(operations) return { "supported": not missing and reason is None, "required_operations": sorted(required), "missing_operations": sorted(missing), - "services": body["provides"], + "services": sandbox.get("services", {}), **({"reason": reason} if reason else {}), } diff --git a/client-sdk/tests/template_fixtures.py b/client-sdk/tests/template_fixtures.py index 3839b0c8..070cb37d 100644 --- a/client-sdk/tests/template_fixtures.py +++ b/client-sdk/tests/template_fixtures.py @@ -1,19 +1,23 @@ -"""Current template fixture factory; no schema migration occurs at runtime.""" +"""Canonical public v1 fixture; no runtime schema migration.""" from copy import deepcopy MANIFEST = { "name": "test", - "spec_version": 5, - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": ["commands.list", "commands.run"], - } + "specVersion": 1, + "sandbox": { + "provider": "alicloud-express", + "template": {"source": {"dockerfile": "Dockerfile"}}, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": ["commands.list", "commands.run"], + } + }, + "start": "python commands.py", }, - "start_cmd": "python commands.py", } diff --git a/client-sdk/tests/test_api/test_capability.py b/client-sdk/tests/test_api/test_capability.py index ed8483c0..bd0a297f 100644 --- a/client-sdk/tests/test_api/test_capability.py +++ b/client-sdk/tests/test_api/test_capability.py @@ -46,12 +46,13 @@ async def test_unknown_remote_reference_is_unknown_not_default(): async def test_explicit_manifest_is_strict_and_resources_do_not_mutate_input(tmp_path): path = tmp_path / "template.yaml" - data = template_manifest(resources={"cpu": 2, "memory": 512}) + data = template_manifest() + data["sandbox"]["template"]["props"] = {"cpu": 2, "memory": 2048} path.write_text(yaml.safe_dump(data)) result = await resolve_capabilities("fixture", local_yaml_path=str(path)) assert result.matched and result.capabilities == {"ports"} parsed = parse_template_data(data) - assert parsed.cpu_count == 2 and parsed.memory_mb == 512 + assert parsed.cpu_count == 2 and parsed.memory_mb == 2048 assert "cpu_count" not in data @@ -72,10 +73,12 @@ async def test_cached_alias_matches_only_valid_manifest(tmp_path, monkeypatch): monkeypatch.setattr("agent_sandbox.api.capability.TEMPLATE_CACHE_DIR", tmp_path) path = tmp_path / "cache" / "template.yaml" path.parent.mkdir() - path.write_text(yaml.safe_dump(template_manifest(aliases=["alias"]))) + data = template_manifest() + data["sandbox"]["template"]["aliases"] = ["alias"] + path.write_text(yaml.safe_dump(data)) result = await resolve_capabilities_for("not-matching", "alias") assert result.matched and "code" not in result.capabilities - path.write_text("name: old\nalias: alias\ncapabilities: [code]") + path.write_text("name: old\nsandbox:\n template:\n aliases: [alias]\ncapabilities: [code]") with pytest.raises(TemplateParseError): await resolve_capabilities("alias") @@ -83,12 +86,18 @@ async def test_cached_alias_matches_only_valid_manifest(tmp_path, monkeypatch): def test_application_resources_are_preserved_as_topology_metadata(tmp_path): from agent_sandbox.utils.registry import load_template_from_yaml - data = template_manifest( - application={"name": "service"}, resources={"cpu": 0.5, "memory": "1Gi"} - ) + topology = { + "edition": "3.0.0", + "resources": {"web": {"component": "fc3@0.0.0", "props": {"cpu": 0.5}}}, + } + data = {"specVersion": 1, "name": "serving", "deployment": topology} path = tmp_path / "template.yaml" path.write_text(yaml.safe_dump(data)) for parsed in (parse_template_data(data), load_template_from_yaml(path)): - assert parsed.model_extra["resources"] == data["resources"] + assert parsed.model_extra["deployment"] == { + **topology, + "name": "serving", + "access": "default", + } assert parsed.cpu_count is None and parsed.memory_mb is None assert "cpu_count" not in data diff --git a/client-sdk/tests/test_api/test_template_launch.py b/client-sdk/tests/test_api/test_template_launch.py index 0acef506..76a30b47 100644 --- a/client-sdk/tests/test_api/test_template_launch.py +++ b/client-sdk/tests/test_api/test_template_launch.py @@ -12,10 +12,10 @@ def provider(operations=()): return SimpleNamespace( - id="fixture", + id="alicloud-express", content_digest=lambda: "sha256:" + "a" * 64, describe=lambda: { - "id": "fixture", + "id": "alicloud-express", "operation_contract_version": 1, "operations": {"cli": list(operations), "python": list(operations)}, "workflow_inputs": ["source", "image", "managed"], @@ -26,11 +26,11 @@ def provider(operations=()): def manifest(input="image"): result = template_manifest() - result.pop("start_cmd", None) + result["sandbox"].pop("start", None) if input == "image": - result["image"] = "registry/image@sha256:" + "f" * 64 + result["sandbox"]["template"] = {"image": "registry/image@sha256:" + "f" * 64} elif input == "managed": - result.update(provider="fixture", template_id="base") + result["sandbox"]["template"] = {"reference": "base"} return result @@ -121,7 +121,7 @@ async def test_protocol_mismatch_never_sends_application_payload_and_cleans( async def test_managed_exec_has_no_application_startup(tmp_path, monkeypatch): body = manifest("managed") - body["provides"] = {} + body["sandbox"]["services"] = {} sandbox = SimpleNamespace( id="owned", provider="fixture", @@ -145,7 +145,7 @@ async def test_managed_exec_has_no_application_startup(tmp_path, monkeypatch): async def test_managed_foreign_provider_rejects_before_create(tmp_path, monkeypatch): body = manifest("managed") - body["provider"] = "foreign-provider" + body["sandbox"]["provider"] = "foreign-provider" create = AsyncMock() monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) p = provider(["sandbox.create", "sandbox.delete", "ports.resolve"]) @@ -267,7 +267,7 @@ async def test_journal_failure_never_blocks_owned_cleanup(tmp_path): from agent_sandbox.api.template import TemplateSession, _Journal journal = _Journal(tmp_path / "run") - journal.append("created", sandbox_id="owned", provider_id="fixture") + journal.append("created", sandbox_id="owned", provider_id="alicloud-express") journal.append = lambda *args, **kwargs: (_ for _ in ()).throw(OSError("disk full")) sandbox = SimpleNamespace(id="owned", provider="fixture", kill=AsyncMock()) server = SimpleNamespace(aclose=AsyncMock()) @@ -292,8 +292,8 @@ async def test_named_builtin_provider_is_not_reregistered(tmp_path, monkeypatch, create = AsyncMock(return_value=sandbox) monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) spec = manifest("managed") - spec["provider"] = "alicloud-express" - spec["provides"] = {} + spec["sandbox"]["provider"] = "alicloud-express" + spec["sandbox"]["services"] = {} async with await launch_template( spec, provider=builtin_provider() if as_object else "alicloud-express", @@ -308,12 +308,10 @@ async def test_named_builtin_provider_is_not_reregistered(tmp_path, monkeypatch, async def test_all_services_start_together_without_invoking_commands(tmp_path, monkeypatch): from agent_sandbox.models.process import ProcessResult - body = template_manifest( - ready_cmd="test -f /tmp/ready", - env={"ABX_SERVER_TOKEN": ""}, - resources={"cpu": 2, "memory": 2048}, - ) - body["provides"]["second"] = {**body["provides"]["app"], "port": 9001} + body = template_manifest() + body["sandbox"]["ready"] = "test -f /tmp/ready" + body["sandbox"]["instance"] = {"envs": {"ABX_SERVER_TOKEN": ""}, "cpu": 2, "memory": 2048} + body["sandbox"]["services"]["second"] = {**body["sandbox"]["services"]["app"], "port": 9001} first, second = AsyncMock(), AsyncMock() for server in (first, second): server.health.return_value = True @@ -339,8 +337,8 @@ async def test_all_services_start_together_without_invoking_commands(tmp_path, m ) as session: assert set(session.services) == set(session.servers) == {"app", "second"} assert session.server is None - start.assert_awaited_once_with(sandbox, body["start_cmd"], 30) - sandbox.commands.run.assert_awaited_once_with(body["ready_cmd"], timeout=30) + start.assert_awaited_once_with(sandbox, body["sandbox"]["start"], 30) + sandbox.commands.run.assert_awaited_once_with(body["sandbox"]["ready"], timeout=30) assert create.call_args.kwargs["envs"]["ABX_SERVER_TOKEN"] == "placeholder-token" assert create.call_args.kwargs["cpu"] == 2 assert create.call_args.kwargs["memory"] == 2048 @@ -357,7 +355,7 @@ async def test_all_services_start_together_without_invoking_commands(tmp_path, m async def test_second_service_failure_closes_every_resolved_client(tmp_path, monkeypatch): body = manifest() - body["provides"]["second"] = {**body["provides"]["app"], "port": 9001} + body["sandbox"]["services"]["second"] = {**body["sandbox"]["services"]["app"], "port": 9001} first, second = AsyncMock(), AsyncMock() for server, version in ((first, 2), (second, 99)): server.health.return_value = True @@ -394,7 +392,7 @@ async def test_fixed_mutable_image_is_rejected_before_create(tmp_path, monkeypat create = AsyncMock() monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) body = manifest() - body["image"] = "image:latest" + body["sandbox"]["template"] = {"image": "image:latest"} with pytest.raises(TemplateLaunchError, match="immutable-image-reference-required"): await launch_template(body, provider=provider(), journal_path=tmp_path / "run") create.assert_not_awaited() @@ -407,7 +405,7 @@ async def test_other_protocol_is_exposed_without_guessing_http_routes( tmp_path, monkeypatch, protocol, version ): body = manifest() - body["provides"]["app"].update(protocol=protocol, version=version) + body["sandbox"]["services"]["app"].update(protocol=protocol, version=version) server = AsyncMock() sandbox = SimpleNamespace( id="owned", @@ -431,7 +429,7 @@ async def test_other_protocol_is_exposed_without_guessing_http_routes( def readiness_launch(tmp_path, monkeypatch): """Keep startup acknowledgement distinct from application readiness.""" body = manifest("managed") - body.update(start_cmd="start application", ready_cmd="check application", provides={}) + body["sandbox"].update(start="start application", ready="check application", services={}) sandbox = SimpleNamespace( id="owned", provider="fixture", @@ -460,7 +458,8 @@ async def test_readiness_retries_early_failure_after_start_acknowledgement(readi startup.assert_awaited_once() assert sandbox.commands.run.await_count == 2 assert all( - call.args == (body["ready_cmd"],) for call in sandbox.commands.run.await_args_list + call.args == (body["sandbox"]["ready"],) + for call in sandbox.commands.run.await_args_list ) sandbox.kill.assert_awaited_once() diff --git a/client-sdk/tests/test_managed_agents.py b/client-sdk/tests/test_managed_agents.py index 0b3289e8..ef5a2f84 100644 --- a/client-sdk/tests/test_managed_agents.py +++ b/client-sdk/tests/test_managed_agents.py @@ -405,6 +405,78 @@ def unavailable(*args: Any, **kwargs: Any) -> Any: thread.join(timeout=3) +@pytest.mark.parametrize("components", ["sqlite", "postgres"], indirect=True) +def test_real_http_session_projection_preserves_private_clock_and_rejects_unavailable_actions( + components: Any, +) -> None: + service = components[0] + token = "projection-test-business-token" + auth = TokenAuthenticator( + { + "tokens": [ + { + "sha256": hashlib.sha256(token.encode()).hexdigest(), + "owner": "owner", + "scopes": ["agents:read", "agents:write"], + } + ] + } + ) + server = make_server(service, auth, port=0) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with httpx.Client( + base_url=f"http://127.0.0.1:{server.server_port}", + headers={"Authorization": "Bearer " + token}, + trust_env=False, + ) as api: + created = api.post("/v1/agents/sessions", json={"agent": {"model": "fixture-model"}}) + assert created.status_code == 201 + for key in ("created_at", "last_active_at"): + assert type(created.json()[key]) is int + assert created.json()["required_actions"] == [] + assert created.json()["vault_ids"] == [] + sid = created.json()["id"] + precise = { + "created_at": 1700000000.125, + "last_active_at": 1700000001.875, + "_deadline": 4700000000.375, + } + with service.store.transaction() as tx: + row = tx.get(sid, "owner") + row.update(precise) + tx.put(row) + for response in (api.get(f"/v1/agents/sessions/{sid}"), api.get("/v1/agents/sessions")): + assert response.status_code == 200 + value = response.json() + if "data" in value: + value = next(item for item in value["data"] if item["id"] == sid) + assert value["created_at"] == 1700000000 + assert value["last_active_at"] == 1700000001 + assert value["required_actions"] == value["vault_ids"] == [] + assert "_deadline" not in value + assert "remote_url" not in value["environment"] + with service.store.transaction() as tx: + stored = tx.get(sid, "owner") + assert {key: stored[key] for key in precise} == precise + assert "required_actions" not in stored and "vault_ids" not in stored + for unsupported in ({"vault_ids": ["not-supported"]}, {"required_actions": []}): + rejected = api.post( + "/v1/agents/sessions", + json={ + "agent": {"model": "fixture-model"}, + **unsupported, + }, + ) + assert rejected.status_code == 400 + assert components[1].created == 0 + finally: + server.shutdown() + server.server_close() + thread.join(timeout=3) + + def test_create_replay_busy_queue_and_owner_boundaries(components: Any) -> None: service, backend, _, _ = components session = create(service, "first", "initial") diff --git a/client-sdk/tests/test_models/test_template.py b/client-sdk/tests/test_models/test_template.py index 76039359..2874465a 100644 --- a/client-sdk/tests/test_models/test_template.py +++ b/client-sdk/tests/test_models/test_template.py @@ -12,17 +12,25 @@ def test_complete_template(self) -> None: from tests.template_fixtures import template_manifest tmpl = SandboxTemplate.model_validate(template_manifest()) - assert tmpl.spec_version == 5 + assert tmpl.spec_version == 1 assert "workflows" not in SandboxTemplate.model_fields - assert tmpl.env == {} and tmpl.ports == [] + assert tmpl.env == {} and tmpl.ports == [9000] assert "code" not in tmpl.discovery_capabilities() def test_application_topology_round_trips(self) -> None: - from tests.template_fixtures import template_manifest - topology = {"workers": {"limits": {"cpu": 0.5}}} - tmpl = SandboxTemplate.model_validate(template_manifest(deployment=topology)) - assert tmpl.model_dump()["deployment"] == topology + topology = { + "edition": "3.0.0", + "resources": {"web": {"component": "fc3@0.0.0", "props": {"cpu": 0.5}}}, + } + tmpl = SandboxTemplate.model_validate( + {"specVersion": 1, "name": "serving", "deployment": topology} + ) + assert tmpl.model_dump()["deployment"] == { + **topology, + "name": "serving", + "access": "default", + } def test_retired_build_fields_are_rejected(self) -> None: import pytest diff --git a/client-sdk/tests/test_models/test_template_server_block.py b/client-sdk/tests/test_models/test_template_server_block.py index 974eb716..f44eeb9f 100644 --- a/client-sdk/tests/test_models/test_template_server_block.py +++ b/client-sdk/tests/test_models/test_template_server_block.py @@ -1,4 +1,4 @@ -"""``SandboxTemplate.server`` (declarative SandboxServer policy).""" +"""Standalone SandboxServer policy remains independent of the v1 resource manifest.""" from __future__ import annotations @@ -28,8 +28,8 @@ } -def make(server: object) -> SandboxTemplate: - return SandboxTemplate.model_validate(template_manifest(server=server)) +def make(server): + return validate_server_block(server) def test_default_is_none() -> None: @@ -37,12 +37,13 @@ def test_default_is_none() -> None: def test_valid_block_round_trips() -> None: - assert make(VALID).server == VALID + assert make(VALID) == VALID def test_existing_templates_without_server_are_unaffected() -> None: - t = SandboxTemplate.model_validate(template_manifest(unknown_future_key=1)) - assert t.server is None + for key in ("unknown_future_key", "server"): + with pytest.raises(ValidationError): + SandboxTemplate.model_validate(template_manifest(**{key: VALID})) @pytest.mark.parametrize( @@ -67,7 +68,7 @@ def test_existing_templates_without_server_are_unaffected() -> None: ], ) def test_malformed_blocks_are_rejected(block: dict[str, object], message: str) -> None: - with pytest.raises(ValidationError, match=message.replace(".", r"\.")): + with pytest.raises(ValueError, match=message.replace(".", r"\.")): make(block) @@ -76,13 +77,11 @@ def test_malformed_blocks_are_rejected(block: dict[str, object], message: str) - ) def test_groups_accept_a_list_or_a_map_and_keep_their_shape(groups: object) -> None: template = make({"groups": groups}) - assert template.server == {"groups": groups} - dumped = template.model_dump(mode="json")["server"] - assert dumped == {"groups": groups} + assert template == {"groups": groups} def test_non_mapping_server_is_rejected() -> None: - with pytest.raises(ValidationError): + with pytest.raises(ValueError): make("transfer-only") @@ -103,7 +102,7 @@ def test_vocabulary_matches_the_server() -> None: def test_a_valid_block_is_accepted_by_the_real_server_loader() -> None: policy = pytest.importorskip("agent_sandbox.server.policy") - block = make(VALID).server + block = make(VALID) assert block is not None policy.ServerPolicy.from_dict(block) @@ -111,7 +110,7 @@ def test_a_valid_block_is_accepted_by_the_real_server_loader() -> None: @pytest.mark.parametrize("groups", [["file_ops"], {"process": False}]) def test_group_forms_are_accepted_by_the_real_server_loader(groups: object) -> None: policy = pytest.importorskip("agent_sandbox.server.policy") - block = make({"groups": groups}).server + block = make({"groups": groups}) assert block is not None assert policy.ServerPolicy.from_dict(block).groups == ( {"file_ops": True} if isinstance(groups, list) else groups diff --git a/client-sdk/tests/test_template_v1_runtime.py b/client-sdk/tests/test_template_v1_runtime.py new file mode 100644 index 00000000..f6919f3a --- /dev/null +++ b/client-sdk/tests/test_template_v1_runtime.py @@ -0,0 +1,696 @@ +"""v1 consumers forward real wire fields and keep Worker environment out of state.""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest +from jsonschema import Draft202012Validator + +from agent_sandbox.api.template import TemplateLaunchError, launch_template +from agent_sandbox.managed_agents import ManagedConfig, ManagedService, ManagedStore +from agent_sandbox.models.managed_instance import worker_environment +from agent_sandbox.models.template import SandboxTemplate +from agent_sandbox.providers.alicloud_express.managed import ExpressBackend +from agent_sandbox.providers.alicloud_express.provider import AlicloudExpressProvider +from agent_sandbox.template_contract.normalize import normalize_template_v1, schema_for_stage +from agent_sandbox.transport.config import TransportConfig +from agent_sandbox.workflows.launch import plan_template_launch + +_CONTRACT_ROOT = Path(__file__).parents[1] / "src/agent_sandbox/template_contract" +_FIXTURES = json.loads((_CONTRACT_ROOT / "fixtures.json").read_text()) + + +@pytest.mark.parametrize( + "case", _FIXTURES["validTemplates"] + _FIXTURES["invalidTemplates"], ids=lambda case: case["id"] +) +def test_owner_admission_vectors_run_with_the_installed_sdk(case): + schema = json.loads((_CONTRACT_ROOT / "template-v1.schema.json").read_text()) + validator = Draft202012Validator(schema_for_stage(schema, case["stage"])).validate + kwargs = {"stage": case["stage"], "provider": case.get("provider")} + if case.get("descriptor"): + # The owner fixture's actual plugin descriptor; validates JSON Schema, + # rather than claiming backend support from a no-op callback. + def descriptor(stage, value, provider): + assert provider == "third-party" and stage == "sandbox.create" + Draft202012Validator( + { + "type": "object", + "additionalProperties": False, + "properties": { + "timeout": {"type": "integer", "minimum": 1}, + "envs": {"type": "object"}, + "metadata": {"type": "object"}, + }, + "required": ["timeout"], + } + ).validate(value) + + kwargs["validate_provider"] = descriptor + if case in _FIXTURES["invalidTemplates"]: + with pytest.raises(ValueError): + normalize_template_v1(case["manifest"], validator, **kwargs) + else: + normalize_template_v1(case["manifest"], validator, **kwargs) + + +def literal_manifest(instance=None): + return { + "specVersion": 1, + "name": "literal", + "sandbox": { + "provider": "alicloud-express", + "template": {"reference": "prepared"}, + **({"instance": instance} if instance is not None else {}), + }, + } + + +async def infrastructure(): + return await AlicloudExpressProvider().build_infra( + TransportConfig( + provider="alicloud-express", + sandbox_api_key="placeholder-platform-key", + sandbox_base_url="https://sandbox-test.example.com", + sandbox_domain="sandbox-test.example.com", + region="cn-hangzhou", + http2=False, + ) + ) + + +def cloud_response(timeout=300): + return { + "sandboxID": "owned", + "templateID": "prepared", + "timeout": timeout, + "envdAccessToken": "placeholder-envd-token", + "envdVersion": "0.3.0", + "trafficAccessToken": "placeholder-traffic-token", + } + + +@pytest.mark.parametrize("override", [False, True]) +async def test_prepared_literal_forwards_instance_through_actual_http_serializer( + tmp_path, + monkeypatch, + httpx_mock, + override, +): + infra = await infrastructure() + monkeypatch.setattr("agent_sandbox.api.sandbox._provider_infra", AsyncMock(return_value=infra)) + httpx_mock.add_response( + method="POST", url="https://sandbox-test.example.com/sandboxes", json=cloud_response() + ) + httpx_mock.add_response( + method="DELETE", url="https://sandbox-test.example.com/sandboxes/owned", status_code=204 + ) + instance = { + "timeout": 720, + "cpu": 2, + "memory": 2048, + "disk": 4096, + "gpu": "A10", + "volume_mounts": {"/data": "borrowed-data"}, + "envs": {"CUSTOM_VALUE": "placeholder-private-value"}, + "metadata": {"purpose": "literal"}, + } + original = copy.deepcopy(instance) + options = ( + { + "timeout": 900, + "cpu": 4, + "memory": 4096, + "disk": 8192, + "gpu": "A100", + "volume_mounts": {"/other": "borrowed-other"}, + "envs": {"CUSTOM_VALUE": "placeholder-override-value"}, + "metadata": {"purpose": "override"}, + } + if override + else {} + ) + journal = tmp_path / "run.jsonl" + async with await launch_template( + literal_manifest(instance), provider="alicloud-express", journal_path=journal, **options + ) as session: + assert "placeholder-private-value" not in json.dumps(session.plan) + assert "placeholder-override-value" not in json.dumps(session.plan) + request = httpx_mock.get_requests(method="POST")[0] + body = json.loads(request.content) + for wire, native in { + "timeout": "timeout", + "cpuCount": "cpu", + "memoryMB": "memory", + "diskSizeMB": "disk", + "gpu": "gpu", + }.items(): + assert body[wire] == options.get(native, instance[native]) + assert body["volumeMounts"] == ( + [{"path": "/other", "name": "borrowed-other"}] + if override + else [{"path": "/data", "name": "borrowed-data"}] + ) + assert body["envVars"] == options.get("envs", instance["envs"]) + assert body["metadata"]["purpose"] == ("override" if override else "literal") + assert body["metadata"]["abx_template_run"] + assert instance == original + assert "placeholder-private-value" not in journal.read_text() + assert "placeholder-override-value" not in journal.read_text() + assert [r.method for r in httpx_mock.get_requests()] == ["POST", "DELETE"] + + +async def test_prepared_literal_default_timeout_is_300(tmp_path, monkeypatch): + sandbox = SimpleNamespace(id="owned", provider="alicloud-express", kill=AsyncMock()) + create = AsyncMock(return_value=sandbox) + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + async with await launch_template( + literal_manifest(), provider="alicloud-express", journal_path=tmp_path / "run" + ): + assert create.call_args.kwargs["timeout"] == 300 + assert "cpu" not in create.call_args.kwargs + assert "memory" not in create.call_args.kwargs + + +async def test_additional_ports_resolve_gateway_endpoints_without_guessing_services( + tmp_path, + monkeypatch, + httpx_mock, +): + infra = await infrastructure() + monkeypatch.setattr("agent_sandbox.api.sandbox._provider_infra", AsyncMock(return_value=infra)) + httpx_mock.add_response( + method="POST", url="https://sandbox-test.example.com/sandboxes", json=cloud_response() + ) + httpx_mock.add_response( + method="DELETE", url="https://sandbox-test.example.com/sandboxes/owned", status_code=204 + ) + manifest = literal_manifest() + manifest["sandbox"]["ports"] = [9222, 5432, 6080] + journal = tmp_path / "run" + async with await launch_template( + manifest, provider="alicloud-express", journal_path=journal + ) as session: + assert set(session.endpoints) == {5432, 6080, 9222} + assert session.services == session.servers == {} + assert session.server is None + for port, endpoint in session.endpoints.items(): + assert endpoint["port"] == port + assert endpoint["url"] == f"https://{port}-owned.sandbox-test.example.com" + assert endpoint["authentication_audience"] == "gateway" + assert endpoint["headers"] == {"e2b-traffic-access-token": "placeholder-traffic-token"} + assert "placeholder-traffic-token" not in json.dumps(session.plan) + assert "ports.resolve" in session.plan["required_operations"] + assert "ports" not in json.loads(httpx_mock.get_request().content) + assert "placeholder-traffic-token" not in journal.read_text() + assert [request.method for request in httpx_mock.get_requests()] == ["POST", "DELETE"] + + +def test_additional_ports_union_and_provider_preflight(): + from agent_sandbox.template_contract import compile_literal_document + + manifest = literal_manifest() + manifest["sandbox"]["ports"] = [6080, 9000] + manifest["sandbox"]["services"] = { + "app": { + "port": 9000, + "protocol": "agent-sandbox-server", + "version": 2, + } + } + assert SandboxTemplate.model_validate(manifest).ports == [6080, 9000] + assert compile_literal_document(manifest, provider="alicloud-express")["ports"] == [6080, 9000] + manifest["sandbox"]["services"] = {} + assert SandboxTemplate.model_validate(manifest).discovery_capabilities() == {"ports"} + plan = plan_template_launch( + manifest, + { + "id": "alicloud-express", + "operation_contract_version": 1, + "operations": {"python": ["sandbox.create", "sandbox.delete"]}, + "workflow_inputs": ["managed"], + "startup_modes": ["none"], + }, + "python-provider", + prepared=True, + ) + assert not plan["supported"] + assert plan["missing_operations"] == ["ports.resolve"] + + +async def test_raw_endpoint_resolution_failure_keeps_owned_cleanup( + tmp_path, monkeypatch, httpx_mock +): + infra = await infrastructure() + monkeypatch.setattr("agent_sandbox.api.sandbox._provider_infra", AsyncMock(return_value=infra)) + httpx_mock.add_response( + method="POST", url="https://sandbox-test.example.com/sandboxes", json=cloud_response() + ) + httpx_mock.add_response( + method="DELETE", url="https://sandbox-test.example.com/sandboxes/owned", status_code=204 + ) + endpoint = AsyncMock( + return_value={ + "provider_id": "foreign", + "sandbox_id": "owned", + "port": 5432, + "url": "https://foreign.example.com", + "headers": {}, + "authentication_audience": "gateway", + "path_mode": "prefix", + } + ) + monkeypatch.setattr(AlicloudExpressProvider, "application_endpoint", endpoint) + manifest = literal_manifest() + manifest["sandbox"]["ports"] = [5432] + with pytest.raises(ValueError, match="does not match"): + await launch_template(manifest, provider="alicloud-express", journal_path=tmp_path / "run") + assert [request.method for request in httpx_mock.get_requests()] == ["POST", "DELETE"] + assert "cleanup-complete" in (tmp_path / "run").read_text() + + +async def test_explicit_api_override_replaces_an_expression_without_evaluating_it( + tmp_path, monkeypatch +): + sandbox = SimpleNamespace(id="owned", provider="alicloud-express", kill=AsyncMock()) + create = AsyncMock(return_value=sandbox) + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + manifest = literal_manifest( + {"timeout": "${env(TTL)}", "envs": {"VALUE": "${env(PRIVATE_VALUE)}"}} + ) + async with await launch_template( + manifest, + provider="alicloud-express", + timeout=750, + envs={"VALUE": "placeholder-explicit-value"}, + journal_path=tmp_path / "run", + ): + assert create.call_args.kwargs["timeout"] == 750 + assert create.call_args.kwargs["envs"] == {"VALUE": "placeholder-explicit-value"} + + +async def test_invalid_explicit_override_fails_before_journaling(tmp_path, monkeypatch): + create = AsyncMock() + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + with pytest.raises(ValueError, match="schema validation failed"): + await launch_template( + literal_manifest(), provider="alicloud-express", cpu=0, journal_path=tmp_path / "run" + ) + create.assert_not_awaited() + assert not (tmp_path / "run").exists() + + +@pytest.mark.parametrize( + "field,value", + [ + ("timeout", "${env(TTL)}"), + ("envs", {"VALUE": "${env(PRIVATE_VALUE)}"}), + ("volume_mounts", {"/data": "${env(VOLUME)}"}), + ], +) +async def test_unresolved_native_expressions_fail_before_create_or_journal( + tmp_path, monkeypatch, field, value +): + create = AsyncMock() + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + with pytest.raises(ValueError, match="native expressions require the Devs parser"): + await launch_template( + literal_manifest({field: value}), + provider="alicloud-express", + journal_path=tmp_path / "run", + ) + create.assert_not_awaited() + assert not (tmp_path / "run").exists() + + +async def test_unsupported_lifecycle_does_not_disappear(tmp_path, monkeypatch): + create = AsyncMock() + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + with pytest.raises(TemplateLaunchError, match="instance-lifecycle-unimplemented"): + await launch_template( + literal_manifest({"lifecycle": {"onTimeout": "pause", "autoResume": True}}), + provider="alicloud-express", + journal_path=tmp_path / "run", + ) + create.assert_not_awaited() + + +async def test_mixed_generic_intent_is_valid_but_sdk_rejects_before_creation(tmp_path, monkeypatch): + from agent_sandbox.template_contract import normalize_document + + manifest = literal_manifest() + manifest["deployment"] = { + "edition": "3.0.0", + "resources": {"web": {"component": "fc3@0.1.26", "props": {"region": "cn-hangzhou"}}}, + } + normalized = normalize_document(manifest) + assert normalized["sandbox"]["template"]["reference"] == "prepared" + assert normalized["deployment"]["resources"]["web"]["component"] == "fc3@0.1.26" + create = AsyncMock() + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + with pytest.raises(TemplateLaunchError, match="deployment-orchestrator-required"): + await launch_template(manifest, provider="alicloud-express", journal_path=tmp_path / "run") + create.assert_not_awaited() + assert not (tmp_path / "run").exists() + + +def runtime_config(**overrides): + return ManagedConfig.from_dict( + { + "revision": "revision-a", + "template": "prepared", + "models": {"fixture-model": "qwen-code"}, + "modelEnvAllowlist": ["OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL"], + **overrides, + } + ) + + +@pytest.mark.parametrize("database", ["sqlite", "postgres"]) +def test_managed_session_keeps_instance_snapshot_and_excludes_environment_values( + tmp_path, database +): + postgres = None + if database == "postgres": + pgserver = pytest.importorskip("pgserver") + postgres = pgserver.get_server(tmp_path / "postgres", cleanup_mode="delete") + url = postgres.get_uri() + else: + url = "sqlite:///" + str(tmp_path / "state.db") + config = runtime_config( + instance={ + "timeout": 1800, + "cpu": 2, + "memory": 2048, + "volume_mounts": {"/data": "borrowed-data"}, + "metadata": {"purpose": "dataset"}, + }, + instanceEnvNames=["CUSTOM_VALUE"], + ) + try: + service = ManagedService(ManagedStore(url, local=True), config) + status, session, _ = service.handle( + "owner", "POST", "/v1/agents/sessions", {"agent": {"model": "fixture-model"}} + ) + assert status == 201 + with service.store.transaction() as tx: + saved = tx.get(session["id"], "owner") + assert saved["_settings"]["instance"] == config.instance + assert saved["_settings"]["instanceEnvNames"] == ["CUSTOM_VALUE"] + assert saved["_settings"]["sessions"]["maxLifetimeSeconds"] == 1800 + assert "envs" not in saved["_settings"]["instance"] + # Updating a later revision's settings cannot rewrite an existing Session. + config.instance["cpu"] = 4 + with service.store.transaction() as tx: + assert tx.get(session["id"], "owner")["_settings"]["instance"]["cpu"] == 2 + private = worker_environment( + {"CUSTOM_VALUE": "placeholder-private-value"}, ["CUSTOM_VALUE"] + ) + assert private["CUSTOM_VALUE"] not in json.dumps(saved) + assert private["CUSTOM_VALUE"] not in json.dumps(session) + finally: + if postgres: + postgres.cleanup() + + +def test_managed_default_and_single_lifetime_source(): + assert runtime_config().instance["timeout"] == 3600 + config = runtime_config(instance={"timeout": 1500}) + assert config.sessions["maxLifetimeSeconds"] == config.instance["timeout"] == 1500 + config = runtime_config( + instance={"timeout": 120}, + sessions={"idleTimeoutSeconds": 60}, + limits={"maxRunSeconds": 90, "maxToolSeconds": 30}, + ) + assert config.sessions["maxLifetimeSeconds"] == 120 + with pytest.raises(ValueError, match="must equal"): + runtime_config(instance={"timeout": 1500}, sessions={"maxLifetimeSeconds": 3600}) + + +@pytest.mark.parametrize( + "instance", + [ + {"envs": {"CUSTOM_VALUE": "placeholder-private-value"}}, + {"secure": False}, + {"lifecycle": {"onTimeout": "pause", "autoResume": True}}, + {"metadata": {"abx-session": "foreign"}}, + *[ + {"volume_mounts": {path: "borrowed-data"}} + for path in ["/workspace", "/workspace/data", "/home", "/home/user"] + ], + ], +) +def test_managed_rejects_conflicting_or_unhandled_instance_input(instance): + with pytest.raises(ValueError): + runtime_config(instance=instance) + + +@pytest.mark.parametrize( + "name", + [ + "ABX_SERVER_TOKEN", + "ABX_MANAGED_STATE_DIR", + "OPENAI_API_KEY", + "PATH", + "PYTHONPATH", + "QWEN_AUTH_TOKEN", + ], +) +def test_managed_custom_env_cannot_override_runtime_or_models(name): + with pytest.raises(ValueError, match="runtime/model controls"): + runtime_config(instanceEnvNames=[name]) + + +async def test_managed_backend_serializes_instance_and_injects_worker_private_env( + monkeypatch, httpx_mock +): + provider = { + "provider": "alicloud-express", + "apiKey": "placeholder-platform-key", + "baseUrl": "https://sandbox-test.example.com", + "domain": "sandbox-test.example.com", + "region": "cn-hangzhou", + } + model = { + "apiKey": "placeholder-model-key", + "baseUrl": "https://model-test.example.com", + "model": "fixture-model", + "adapter": "qwen-code", + } + backend = ExpressBackend( + provider, model, instance_env={"CUSTOM_VALUE": "placeholder-private-value"} + ) + infra = await infrastructure() + + class Reader: + cancel = AsyncMock() + + def __aiter__(self): + return self.events() + + async def events(self): + yield SimpleNamespace(pid=123, exit_code=None) + + start = AsyncMock(return_value=Reader()) + infra.process_protocol = SimpleNamespace(start=start) + monkeypatch.setattr(backend, "_infra", AsyncMock(return_value=infra)) + httpx_mock.add_response( + method="POST", url="https://sandbox-test.example.com/sandboxes", json=cloud_response(1800) + ) + config = runtime_config( + instance={ + "timeout": 1800, + "cpu": 4, + "memory": 4096, + "disk": 8192, + "gpu": "A10", + "volume_mounts": {"/data": "borrowed-data"}, + "metadata": {"purpose": "dataset"}, + }, + instanceEnvNames=["CUSTOM_VALUE"], + ) + settings = { + "template": config.template, + "workspace": config.workspace, + "port": config.port, + "sessions": config.sessions, + "modelEnvAllowlist": config.model_env_allowlist, + "instance": config.instance, + "instanceEnvNames": config.instance_env_names, + } + session = { + "id": "session-a", + "_revision": "revision-a", + "_settings": settings, + "agent": {"model": "fixture-model", "x_agent_sandbox": {"adapter": "qwen-code"}}, + } + receipts = [] + await backend.create(session, "placeholder-app-token", receipts.append) + body = json.loads(httpx_mock.get_request().content) + assert { + key: body[key] for key in ["timeout", "cpuCount", "memoryMB", "diskSizeMB", "gpu", "secure"] + } == { + "timeout": 1800, + "cpuCount": 4, + "memoryMB": 4096, + "diskSizeMB": 8192, + "gpu": "A10", + "secure": True, + } + assert body["autoPause"] is False + assert body["volumeMounts"] == [{"name": "borrowed-data", "path": "/data"}] + assert body["metadata"] == { + "purpose": "dataset", + "abx-managed": "true", + "abx-session": "session-a", + "abx-revision": "revision-a", + } + assert body["envVars"] == { + "CUSTOM_VALUE": "placeholder-private-value", + "ABX_SERVER_TOKEN": "placeholder-app-token", + "ABX_MANAGED_WORKSPACE": "/workspace", + "OPENAI_API_KEY": "placeholder-model-key", + "OPENAI_BASE_URL": model["baseUrl"], + "OPENAI_MODEL": "fixture-model", + } + assert start.call_args.kwargs["env"]["CUSTOM_VALUE"] == "placeholder-private-value" + assert "placeholder-private-value" not in json.dumps(settings) + assert "placeholder-private-value" not in json.dumps(receipts) + assert "placeholder-model-key" not in json.dumps(receipts) + assert [r.url.path for r in httpx_mock.get_requests()] == ["/sandboxes"] + # Cleanup only recorded owned compute; no Volume deletion API is called. + cleanup_infra = await infrastructure() + monkeypatch.setattr(backend, "_infra", AsyncMock(return_value=cleanup_infra)) + httpx_mock.add_response( + method="DELETE", url="https://sandbox-test.example.com/sandboxes/owned", status_code=204 + ) + httpx_mock.add_response( + method="GET", url="https://sandbox-test.example.com/sandboxes/owned", status_code=404 + ) + assert await backend.kill(receipts[-1]) + assert all("volume" not in r.url.path.lower() for r in httpx_mock.get_requests()) + + +def test_canonical_sources_are_locked_to_immutable_owner_bytes(): + root = Path(__file__).parents[1] / "src/agent_sandbox/template_contract" + lock = json.loads((root / "source.json").read_text()) + assert len(lock["revision"]) == 40 + for entry in lock["files"].values(): + assert hashlib.sha256((root / entry["target"]).read_bytes()).hexdigest() == entry["sha256"] + + +@pytest.mark.parametrize("source", [False, True]) +async def test_selected_plugin_uses_real_sync_schema_without_requiring_plugin_release( + tmp_path, + monkeypatch, + source, +): + seen = [] + + def validate(stage, value): + seen.append((stage, copy.deepcopy(value))) + if stage == "template.prepare": + schema = { + "type": "object", + "additionalProperties": False, + "properties": { + "source": { + "type": "object", + "additionalProperties": False, + "properties": {"dockerfile": {"type": "string"}, "context": {"const": "."}}, + "required": ["dockerfile", "context"], + }, + "props": {"type": "object", "additionalProperties": False}, + }, + "required": ["source"], + } + else: + assert stage == "sandbox.create" + schema = { + "type": "object", + "additionalProperties": False, + "properties": { + "timeout": {"type": "integer", "minimum": 1, "maximum": 1000}, + "envs": {"type": "object"}, + "metadata": {"type": "object"}, + }, + "required": ["timeout"], + } + Draft202012Validator(schema).validate(value) + + plugin = SimpleNamespace( + id="strict-plugin", + validate_template_input=validate, + content_digest=lambda: "sha256:" + "d" * 64, + describe=lambda: { + "id": "strict-plugin", + "operation_contract_version": 1, + "operations": {"python": ["sandbox.create", "sandbox.delete"]}, + "workflow_inputs": ["source", "managed"], + "startup_modes": ["entrypoint", "none"], + }, + ) + sandbox = SimpleNamespace(id="owned", provider="strict-plugin", kill=AsyncMock()) + create = AsyncMock(return_value=sandbox) + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + manifest = literal_manifest({"timeout": 777}) + manifest["sandbox"]["provider"] = plugin.id + if source: + manifest["sandbox"]["template"] = {"source": {"dockerfile": "Dockerfile"}} + async with await launch_template( + manifest, provider=plugin, reference="prepared", journal_path=tmp_path / "run" + ): + assert create.call_args.kwargs["provider"] is plugin + assert create.call_args.kwargs["timeout"] == 777 + assert [stage for stage, _ in seen] == ( + ["template.prepare", "sandbox.create"] if source else ["sandbox.create"] + ) + manifest["sandbox"]["instance"]["timeout"] = 1001 + create.reset_mock() + with pytest.raises(ValueError, match="selected Provider validation failed"): + await launch_template( + manifest, provider=plugin, reference="prepared", journal_path=tmp_path / "rejected" + ) + create.assert_not_awaited() + assert not (tmp_path / "rejected").exists() + + +@pytest.mark.parametrize("mode", ["missing", "async"]) +async def test_plugin_without_real_sync_validator_rejects_before_side_effects( + tmp_path, monkeypatch, mode +): + from agent_sandbox.providers.base import Provider + + plugin = SimpleNamespace( + id="strict-plugin", + content_digest=lambda: "sha256:" + "d" * 64, + describe=lambda: { + "id": "strict-plugin", + "operation_contract_version": 1, + "operations": {"python": ["sandbox.create", "sandbox.delete"]}, + "workflow_inputs": ["managed"], + "startup_modes": ["none"], + }, + ) + if mode == "async": + + async def validate(stage, value): + raise AssertionError("An async validator must never execute") + + plugin.validate_template_input = validate + else: + # An inherited default is explicitly not counted as a real validator. + plugin.validate_template_input = Provider.validate_template_input.__get__(plugin) + manifest = literal_manifest() + manifest["sandbox"]["provider"] = plugin.id + create = AsyncMock() + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + with pytest.raises(ValueError, match="real preparation/create validators|validation failed"): + await launch_template(manifest, provider=plugin, journal_path=tmp_path / "run") + create.assert_not_awaited() + assert not (tmp_path / "run").exists() diff --git a/client-sdk/tests/test_template_yaml_reader.py b/client-sdk/tests/test_template_yaml_reader.py new file mode 100644 index 00000000..b7e635f5 --- /dev/null +++ b/client-sdk/tests/test_template_yaml_reader.py @@ -0,0 +1,128 @@ +"""Public v1 file consumers agree on actual YAML Core scalar/admission behavior.""" + +from __future__ import annotations + +import json +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from agent_sandbox import template_contract +from agent_sandbox.api.capability import resolve_capabilities +from agent_sandbox.api.template import launch_template +from agent_sandbox.template_contract.normalize import parse_yaml_v1 +from agent_sandbox.utils.registry import load_template_from_yaml + +LITERAL = b"""specVersion: 1 +name: yaml-values +sandbox: + provider: alicloud-express + template: + reference: base + aliases: [yaml-alias] + instance: + envs: + FLAG: on + WHEN: 2026-11-12 +""" + + +def test_registry_file_loader_preserves_core_strings(tmp_path): + path = tmp_path / "template.yaml" + path.write_bytes(LITERAL) + template = load_template_from_yaml(path) + assert template.env == {"FLAG": "on", "WHEN": "2026-11-12"} + + +async def test_cached_discovery_uses_the_same_core_reader(tmp_path, monkeypatch): + monkeypatch.setattr("agent_sandbox.api.capability.TEMPLATE_CACHE_DIR", tmp_path) + directory = tmp_path / "cached" + directory.mkdir() + (directory / "template.yaml").write_bytes(LITERAL) + result = await resolve_capabilities("yaml-alias") + assert result.matched + + +async def test_prepared_file_launch_forwards_core_strings_without_cli(tmp_path, monkeypatch): + path, journal = tmp_path / "template.yaml", tmp_path / "run" + path.write_bytes(LITERAL) + sandbox = SimpleNamespace(id="local-fixture", provider="alicloud-express", kill=AsyncMock()) + create = AsyncMock(return_value=sandbox) + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + async with await launch_template(path, provider="alicloud-express", journal_path=journal): + assert create.call_args.kwargs["envs"] == {"FLAG": "on", "WHEN": "2026-11-12"} + sandbox.kill.assert_awaited_once() + assert "WHEN" not in journal.read_text() + + +SAFE_LITERAL = LITERAL.replace(b"FLAG: on", b"FLAG: safe").replace( + b"WHEN: 2026-11-12", b'WHEN: "2026-11-12"' +) + + +@pytest.mark.parametrize( + "content", + [ + SAFE_LITERAL.replace( + b"FLAG: safe", b"FLAG: first-private-value\n FLAG: second-private-value" + ), + SAFE_LITERAL.replace(b"FLAG: safe", b"FLAG: &sample literal\n COPY: *sample"), + SAFE_LITERAL + b"\n#" + b"x" * 1048576, + b"""specVersion: 1 +name: yaml-values +sandbox: + provider: alicloud-express + template: {reference: base} +""".decode().encode("utf-16"), + ], +) +async def test_invalid_yaml_is_rejected_before_resource_or_journal(tmp_path, monkeypatch, content): + path, journal = tmp_path / "template.yaml", tmp_path / "run" + path.write_bytes(content) + sandbox = SimpleNamespace(id="local-fixture", provider="alicloud-express", kill=AsyncMock()) + create = AsyncMock(return_value=sandbox) + monkeypatch.setattr("agent_sandbox.api.template.Sandbox.create", create) + session = None + try: + with pytest.raises(ValueError) as caught: + session = await launch_template(path, provider="alicloud-express", journal_path=journal) + finally: + if session is not None: + await session.close() + assert "first-private-value" not in str(caught.value) + assert "second-private-value" not in str(caught.value) + create.assert_not_awaited() + assert not journal.exists() + + +_VECTORS = json.loads(Path(template_contract.__file__).with_name("yaml-fixtures.json").read_text()) + + +@pytest.mark.parametrize("case", _VECTORS["valid"], ids=lambda case: case["id"]) +def test_shared_valid_yaml_text_vectors_use_actual_core_loader(case): + assert parse_yaml_v1(case["text"]) == case["expected"] + assert parse_yaml_v1(case["text"].encode("utf-8")) == case["expected"] + + +@pytest.mark.parametrize("case", _VECTORS["invalid"], ids=lambda case: case["id"]) +def test_shared_invalid_yaml_vectors_reject_without_source_values(case): + source = ( + bytes.fromhex(case["hex"]) + if "hex" in case + else case["repeat"] * case["count"] + if "repeat" in case + else case["text"] + ) + with pytest.raises(ValueError) as caught: + parse_yaml_v1(source) + assert "private-source-canary" not in str(caught.value) + + +def test_core_reader_does_not_mutate_other_pyyaml_consumers(): + import yaml + + assert yaml.safe_load("flag: on")["flag"] is True + assert parse_yaml_v1("flag: on")["flag"] == "on" + assert yaml.safe_load("flag: on")["flag"] is True diff --git a/client-sdk/tests/test_templates/conftest.py b/client-sdk/tests/test_templates/conftest.py index a6cb3e6f..6920a5f1 100644 --- a/client-sdk/tests/test_templates/conftest.py +++ b/client-sdk/tests/test_templates/conftest.py @@ -30,10 +30,8 @@ #: Files every template folder must contain. REQUIRED_TEMPLATE_FILES = ("template.yaml", "Dockerfile", "README.md") -#: Template YAML keys that must be present *explicitly* in every catalog entry. -#: (The Pydantic model gives most of them defaults, but a published template is -#: expected to spell them out so the index table stays meaningful.) -REQUIRED_YAML_KEYS = ("name", "version", "description", "author", "tags") +#: Required resource keys; local fixtures need no Registry publication metadata. +REQUIRED_YAML_KEYS = ("specVersion", "name", "sandbox") def discover_template_dirs() -> list[Path]: diff --git a/client-sdk/tests/test_templates/test_template_catalog.py b/client-sdk/tests/test_templates/test_template_catalog.py index 29674174..df8e128e 100644 --- a/client-sdk/tests/test_templates/test_template_catalog.py +++ b/client-sdk/tests/test_templates/test_template_catalog.py @@ -12,8 +12,8 @@ (a) ``template.yaml`` exists and parses through the *real* production loader (``utils.registry.load_template_from_yaml``) into a valid ``SandboxTemplate``. -(b) Required fields are present; every ``capabilities`` token is inside - ``STANDARD_CAPABILITIES``. +(b) Required v1 resource fields are present; publication metadata stays outside + local resources and discovery does not imply Provider capabilities. (c) ``Dockerfile`` and ``README.md`` exist next to the YAML. (d) The directory is *explicitly marked as a fixture* (README + YAML banner) and contains only the expected fixture folders — it must never @@ -44,7 +44,6 @@ if TYPE_CHECKING: from pathlib import Path -_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$") _PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") @@ -120,24 +119,24 @@ def test_name_is_kebab_case(self, template_dir: Path) -> None: f"{template_dir.name}: template names must be kebab-case" ) - def test_version_is_semver(self, template_dir: Path) -> None: - version = _load(template_dir).version - assert _SEMVER_RE.match(version), ( - f"{template_dir.name}: version {version!r} is not semver-like" - ) - - def test_description_non_empty(self, template_dir: Path) -> None: - assert _load(template_dir).description.strip(), ( - f"{template_dir.name}: description must not be blank" - ) - - def test_tags_non_empty_and_lowercase(self, template_dir: Path) -> None: - tags = _load(template_dir).tags - assert tags, f"{template_dir.name}: declare at least one tag" - for tag in tags: - assert tag == tag.strip().lower(), ( - f"{template_dir.name}: tag {tag!r} should be trimmed lowercase" - ) + @pytest.mark.parametrize( + "key,value", + [ + ("version", "1.0.0"), + ("description", "Fixture description"), + ("tags", ["fixture"]), + ], + ) + def test_publication_fields_are_not_runtime_resource_fields( + self, + template_dir: Path, + key: str, + value: object, + ) -> None: + raw = _raw(template_dir) + assert key not in raw + with pytest.raises(ValidationError): + SandboxTemplate.model_validate({**raw, key: value}) def test_resources_when_declared_are_positive(self, template_dir: Path) -> None: tmpl = _load(template_dir) @@ -162,7 +161,7 @@ class TestWorkflows: def test_template_has_single_startup(self, template_dir: Path) -> None: tmpl = _load(template_dir) - assert tmpl.spec_version == 5 + assert tmpl.spec_version == 1 def test_no_old_capability_input(self, template_dir: Path) -> None: assert "capabilities" not in _raw(template_dir) diff --git a/contracts/launch-v1.json b/contracts/launch-v1.json index b7cbc579..63ee5669 100644 --- a/contracts/launch-v1.json +++ b/contracts/launch-v1.json @@ -1,10 +1,11 @@ { "version": 1, - "template_spec_version": 5, + "template_spec_version": 1, "max_safe_integer": 9007199254740991, "template_keys": [ - "spec_version", - "provides" + "specVersion", + "name", + "sandbox" ], "retired_keys": [ "workflow_contract_version", diff --git a/contracts/launch-vectors.json b/contracts/launch-vectors.json index 3c3e513a..d8d9c870 100644 --- a/contracts/launch-vectors.json +++ b/contracts/launch-vectors.json @@ -4,20 +4,27 @@ "id": "source-cli", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -78,20 +85,27 @@ "id": "source-python-unprepared", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -154,20 +168,27 @@ "id": "source-python-prepared", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -227,29 +248,36 @@ "id": "multiple-services", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } }, - "api": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9001, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + }, + "api": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9001, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -319,10 +347,17 @@ "id": "no-services", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": {}, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": {}, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -372,10 +407,15 @@ "id": "managed-no-start", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "provider": "fixture", - "template_id": "base" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "reference": "base" + }, + "services": {}, + "provider": "fixture" + } }, "provider": { "id": "fixture", @@ -423,10 +463,15 @@ "id": "managed-foreign-provider", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "provider": "other", - "template_id": "base" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "reference": "base" + }, + "services": {}, + "provider": "other" + } }, "provider": { "id": "fixture", @@ -475,19 +520,24 @@ "id": "image-entrypoint", "input": { "manifest": { - "spec_version": 5, - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "image": "registry/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } } - }, - "image": "registry/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } }, "provider": { "id": "fixture", @@ -546,10 +596,15 @@ "id": "ready-command", "input": { "manifest": { - "spec_version": 5, - "provides": {}, - "image": "image", - "ready_cmd": "test -f /tmp/ready" + "specVersion": 1, + "name": "test", + "sandbox": { + "template": { + "image": "image" + }, + "services": {}, + "ready": "test -f /tmp/ready" + } }, "provider": { "id": "fixture", @@ -598,20 +653,27 @@ "id": "unsupported-input", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -671,20 +733,27 @@ "id": "unsupported-startup", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -744,20 +813,27 @@ "id": "missing-port", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -814,20 +890,27 @@ "id": "retired-v4", "input": { "manifest": { - "spec_version": 4, + "specVersion": 4, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -867,20 +950,27 @@ "id": "retired-workflows", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" }, - "start_cmd": "python app.py", "workflows": {} }, "provider": { @@ -921,21 +1011,28 @@ "id": "unknown-service-field", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ], - "binding": {} - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ], + "binding": {} + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -975,21 +1072,24 @@ "id": "bad-provider-binding", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": 9000, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py", - "provider": "fixture" + "sandbox": { + "template": {}, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": 9000, + "operations": [ + "commands.run", + "health" + ] + } + }, + "provider": "fixture", + "start": "python app.py" + } }, "provider": { "id": "fixture", @@ -1029,20 +1129,27 @@ "id": "bad-port-bool", "input": { "manifest": { - "spec_version": 5, + "specVersion": 1, "name": "test", - "provides": { - "app": { - "protocol": "agent-sandbox-server", - "version": 2, - "port": true, - "operations": [ - "commands.run", - "health" - ] - } - }, - "start_cmd": "python app.py" + "sandbox": { + "template": { + "source": { + "dockerfile": "Dockerfile" + } + }, + "services": { + "app": { + "protocol": "agent-sandbox-server", + "version": 2, + "port": true, + "operations": [ + "commands.run", + "health" + ] + } + }, + "start": "python app.py" + } }, "provider": { "id": "fixture", diff --git a/docs/en/design/server-api.md b/docs/en/design/server-api.md index ea800600..d359a90e 100644 --- a/docs/en/design/server-api.md +++ b/docs/en/design/server-api.md @@ -20,7 +20,7 @@ The container-side HTTP Server is a lightweight HTTP service that relies **solel - Each route is an endpoint with one id, declared once in [`endpoints.py`](../../../server-sdk/src/agent_sandbox/server/endpoints.py). A **Capability Group** is a name for a set of endpoint ids. - Request/response objects are stdlib `dataclass`es ([`types.py`](../../../server-sdk/src/agent_sandbox/server/types.py)), not Pydantic models. -> Server endpoint-policy groups are independent from specification 5 template startup and Provider operations. Launch exposes all declared services; actual discovery, policy and token scopes still control each API call. See [Template System](template-system.md). +> Server endpoint-policy groups are independent from specification 1 template startup and Provider operations. Launch exposes all declared services; actual discovery, policy and token scopes still control each API call. See [Template System](template-system.md). ### 1.2 Startup diff --git a/docs/en/design/template-system.md b/docs/en/design/template-system.md index 5c4a4028..17ac6d3c 100644 --- a/docs/en/design/template-system.md +++ b/docs/en/design/template-system.md @@ -1,6 +1,6 @@ # Template System -Templates use specification 5. A template defines the complete sandbox contents, +Templates use specification 1. A template defines the complete sandbox contents, its startup/readiness commands and every service it provides. Starting a template never selects a subset by workflow, operation or application/exec/interpreter type. @@ -20,16 +20,16 @@ operations needed to create, start, inspect readiness and clean up. Missing inte operations never prevent an unrelated application from starting. Actual operations remain guarded by the Provider and server policy at invocation time. -Every `provides` service is retained. SandboxServer protocol v2 endpoints receive protocol checks; +Every `sandbox.services` service is retained. SandboxServer protocol v2 endpoints receive protocol checks; unknown protocols are returned and use the author's readiness command. A multi-service template starts once. Calling one endpoint later does not start a different template. ## Manifest and migration See [the field reference](../reference/template-yaml-spec.md) and the authoritative -[template-v5 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v5.schema.json). +[template-v1 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v1.schema.json). Remove `workflow_contract_version`, `default_workflow` and `workflows`; keep -`spec_version: 5`, `provides`, `start_cmd`, `ready_cmd` and application metadata. +`specVersion: 1`, `sandbox.services`, `sandbox.start`, `sandbox.ready` and application metadata. Old running manifests require explicit migration. Source locations, prepared image references and native template IDs are preparation details, not user-selectable flows. @@ -40,7 +40,7 @@ cleanup attempts every owned client and sandbox, retaining failures for reconcil Connecting to an existing sandbox does not confer deletion ownership. Catalog membership or a static supported plan is not a test pass. Historical workflow -receipts remain readable history; they do not certify specification 5. A template's +receipts remain readable history; they do not certify specification 1. A template's metadata version is not a registry storage/versioning service. Use actual Git revisions and recorded inputs to reproduce a run. diff --git a/docs/en/guide/authoring-templates.md b/docs/en/guide/authoring-templates.md index 88354d1e..f05750c1 100644 --- a/docs/en/guide/authoring-templates.md +++ b/docs/en/guide/authoring-templates.md @@ -1,165 +1,22 @@ # Authoring Templates -For current SDK installation, use the [installation guide](install-python.md). +A template contains application source, a Dockerfile and one `template.yaml`. Use `specVersion: 1`; the [field reference](../reference/template-yaml-spec.md) describes resource, instance and native Serving configuration. Optional `registry.yaml` carries catalog descriptions and initialization questions. Dependencies belong in the Dockerfile and native dependency files. -> **Note:** The `abx` commands shown on this page are provided by the Node.js CLI in the `cli` repository ([CLI binary installation](https://github.com/Alibaba-Agent-Sandbox/agent-sandbox/blob/main/docs/en/guide/binary-installation.md)). CLI-specific documentation (CLI design, tutorial, reference) lives there too; this repository documents the Python SDK (`agent-sandbox-client`) and the container-side runtime (`agent-sandbox-server`). - -This is the **Server SDK**: `commands.py` starts with the image and serves HTTP inside the sandbox. Creating and calling a sandbox from your machine is the [Client SDK](client-sdk.md). - -This document explains how to create custom Agent Sandbox templates, including directory structure, field reference, whole-template startup, registered commands, and publishing. - -A template is a directory of plain files, written by hand or generated. The `abx template init` command generates one three ways (built-in scaffold, a one-sentence description, or adopting a project that already has source code); that workflow is documented in the CLI repository: [Authoring Templates (CLI)](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/en/guide/authoring-templates.md). - ---- - -## Template Directory Structure - -A complete template directory structure is as follows: - -```text -my-template/ -├── template.yaml # Required — template definition -├── Dockerfile # Image build input; no implicit manifest package installer -├── commands.py # Optional — named commands via @command -└── README.md # Optional — template description -``` - -`template.yaml` is the only required file. - ---- - -## template.yaml Basic Example - -```yaml -name: my-python-template -version: 1.0.1 -description: Scaffold for a new Agent Sandbox template. Not a catalog entry. -author: alibaba-agent-sandbox -license: Apache-2.0 -spec_version: 5 -base: fc-e2b-registry..cr.aliyuncs.com/runtime/base: -generation: 2 -start_cmd: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py -ready_cmd: agent-sandbox-server healthcheck --port 9000 -resources: - cpu: 2 - memory: 2048 -ports: -- 9000 -tags: -- example -- skeleton -priority: P2 -wave: other -env: - ABX_SERVER_TOKEN: '' -smoke: -- command: hello - args: - name: Express - expect: Hello, Express! -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - commands.list - - commands.run -``` - ---- - -## Field Reference - -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `name` | `str` | ✅ | — | Template name | -| `version` | `str` | ❌ | `"1.0.0"` | Semantic version | -| `description` | `str` | ❌ | `""` | Template description | -| `env` | `dict[str, str]` | ❌ | `{}` | Environment variables | -| `cpu_count` | `int` | ❌ | `None` | Default CPU cores | -| `memory_mb` | `int` | ❌ | `None` | Default memory in MB | -| `ports` | `list[int]` | ❌ | `[]` | Exposed port list | -| `author` | `str` | ❌ | `""` | Author | -| `license` | `str` | ❌ | `""` | License | -| `tags` | `list[str]` | ❌ | `[]` | Tags | - -For detailed field definitions, see [template.yaml Spec](../reference/template-yaml-spec.md). - ---- - - -## Template startup - -Use `spec_version: 5` and declare every service in `provides`. `start_cmd` and -`ready_cmd` apply to the complete template, without named workflow selections. -The Provider supplies platform operations; service policy controls application APIs. -Neither layer implies interpreter support. See [launching templates](template-workflows.md). - -Server endpoint policies remain independent. The `core` group stays enabled; -`dev_tools`, `browser`, and `agents` require explicit configuration. Declaring an -API in template metadata does not enable its server policy. - ---- - -## Registered Commands - -There is one mechanism for named commands: functions registered in the template's server. `template.yaml` has no `custom_commands` key (the client rejects it). Register Python functions with the decorator in `commands.py`: +For Python application endpoints, install `agent-sandbox-server` in the image and register actual business functions: ```python from agent_sandbox.server import command, start -@command("demo", description="Echo the arguments.") -def demo(x: int, y: str) -> str: - return f"{y}={x}" +@command("hello", description="Greet a user") +def hello(name: str) -> str: + return f"Hello, {name}!" if __name__ == "__main__": start(9000) ``` -File upload and download are server endpoints (`/files/*`, `/upload`, `/download`) of the `file_ops` group, which is on by default. They are not registered from `commands.py`. Deploy does not apply a `server:` block. Turn an opt-in group on with `default_table().enable_group(CapabilityGroup.DEV_TOOLS)` (a `CapabilityGroup`, not a string) or `ABX_SERVER_ENABLED_GROUPS`. `dev_tools`, `browser`, and `agents` start disabled. - -Registered commands are served by the in-sandbox HTTP server (`POST /commands/{name}`). Clients call them with `await sandbox.run("demo", x=1, y="hello")` or `abx sandbox command run demo --arg x=1 --arg y=hello`, and list them with `sandbox.list_commands()` or `abx sandbox command list `. A bare shell command is a different call: `sandbox.commands.run(cmd)`. - ---- - -## Dockerfile Customization - -The `Dockerfile` in the template directory is the image when one is present. `template.yaml` does not install packages. `base` is valid source metadata, but is not an instruction to rewrite `FROM`. Retired manifest package-install fields are rejected; dependencies belong in the effective Dockerfile and native dependency files. - -Keep the source Dockerfile unchanged. Resolve base-image placeholders in the source or explicitly select a build configuration; never silently substitute another image based on the provider. The valid `base` metadata field does not replace Dockerfile `FROM`. - -## Provider template profile - -The server does not select a provider. Port 9000 is the application HTTP listen default; injected envd, interpreter services and reserved ports belong to each backend. Those differences do not imply the application is unusable. A declared `start_cmd` runs once; without it, the image or managed template owns startup. Do not run native startup and a start command twice; actually check a nonempty `ready_cmd`. Build configurations are explicit, and the effective Dockerfile/arguments participate in input identity. - -`commands.py` imports only `agent_sandbox.server` and calls `start(port)` from its -main entry; the image explicitly installs the server SDK. For the provider-native -application API in the published 0.4.0a6 alpha, import `ProviderSandbox` from -`agent_sandbox` and connect with `await ProviderSandbox.connect(ID)`. This instance -supports `async with await sandbox.application_client(9000, token=application_token)`; -the standard `Sandbox`/`AsyncSandbox` classes have a different interface. -Application endpoint resolution preserves proxy prefixes and separates gateway and -application credentials. See the [complete connection/close example](template-server-api.md#reaching-the-sandbox); -the sandbox creator owns its final deletion. - ---- - -## Publishing and Deploying Templates - -The Python SDK is the runtime client and does not build source templates. Normally use `abx --provider PROVIDER deploy SOURCE --name my-app --yes`, then connect Python to the returned sandbox. Advanced image preparation uses `abx template build` and `abx deploy`; before passing a prepared reference to `launch_template`, ensure its native startup does not also start the same service. See [startup ownership](template-workflows.md) and [Build & Deploy](build-and-deploy.md). - -To publish a template to GitHub, install one locally or from a registry, adopt an existing project (`abx template init --adopt`), or build and register one from the command line (`abx deploy`, `abx template build`), use the `abx` CLI; see [Related: the abx CLI](#related-the-abx-cli). - -## Next Steps - -- [Using Templates](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/en/guide/using-templates.md) — Install and use templates -- [template.yaml Spec](../reference/template-yaml-spec.md) — Precise definitions for all fields -- [Declarative Usage](declarative-usage.md) — The @sandbox decorator - ---- +Declare the application's startup/readiness in `sandbox.start` / `sandbox.ready` and endpoints in `sandbox.services`. YAML declarations do not register functions or enable opt-in server groups. Server policy is independent runtime configuration; install its file in the image or use the documented server configuration input. See [the server API](template-server-api.md). -## Related: the abx CLI +Use [CLI authoring](https://github.com/Alibaba-Agent-Sandbox/agent-sandbox/blob/main/docs/en/guide/authoring-templates.md) to prepare and publish source. Python [launches a prepared template](template-workflows.md) or connects to an existing Sandbox. Choose one startup owner; a native template entrypoint and Python startup command must not launch the same service twice. -The command-line workflow for templates is documented in the CLI repository: [Authoring Templates (CLI)](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/en/guide/authoring-templates.md) (`abx template init`, `--adopt`, publishing, `template init --from`), [Deploy & Build](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/en/guide/deploy-and-build.md) (`abx deploy`, `abx template build`) and [Using Templates](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/en/guide/using-templates.md). +Call registered functions through `session.servers["app"].commands.run("hello", name="Python")` or an explicitly resolved `sandbox.application_client`. Application tokens, gateway headers and Sandbox API credentials have separate audiences. Shell execution remains `sandbox.commands.run(...)` and does not imply a business API or interpreter operation. diff --git a/docs/en/guide/best-practices.md b/docs/en/guide/best-practices.md index 5e35c767..a7aa6cc8 100644 --- a/docs/en/guide/best-practices.md +++ b/docs/en/guide/best-practices.md @@ -14,11 +14,13 @@ The server has nine capability groups. `core` stays on (`GET /health`, `GET /cap A group is only a name for the endpoints that declare it; the endpoint rule is the one switch. Turn a default group off in the template policy, as a map: -```yaml -server: - groups: - terminal: false - process: false +```json +{ + "groups": { + "terminal": false, + "process": false + } +} ``` `core` cannot be turned off. Turn an opt-in group on with a policy list (`groups: [dev_tools]`), with `default_table().enable_group(CapabilityGroup.DEV_TOOLS)` in `commands.py`, or with the environment variable `ABX_SERVER_ENABLED_GROUPS=dev_tools`. That variable only turns groups on; there is no variable that turns one off. `GET /capabilities` lists every endpoint with the `rule` that disabled it. diff --git a/docs/en/guide/file-and-command-controls.md b/docs/en/guide/file-and-command-controls.md index 6df1774f..54b81c4b 100644 --- a/docs/en/guide/file-and-command-controls.md +++ b/docs/en/guide/file-and-command-controls.md @@ -1,44 +1,81 @@ # File and command controls -How a template limits which paths and which programs the sandbox server will touch. How to turn the policy on, and the full endpoint catalogue, are in [Fine-grained capabilities](fine-grained-capabilities.md). The HTTP shapes of those routes are in the [template server HTTP API](template-server-api.md). The `server:` block of `template.yaml` is specified in the [template YAML spec](../reference/template-yaml-spec.md#server-policy-server). The design notes are in [Fine-grained capabilities](../design/fine-grained-capabilities.md). +How a template limits which paths and which programs the sandbox server will touch. How to turn the policy on, and the full endpoint catalogue, are in [Fine-grained capabilities](fine-grained-capabilities.md). The HTTP shapes of those routes are in the [template server HTTP API](template-server-api.md). Server policy is loaded independently of the [v1 resource manifest](../reference/template-yaml-spec.md#capability-declarations). The design notes are in [Fine-grained capabilities](../design/fine-grained-capabilities.md). The live routes call `check_env`, `check_url`, and `check_process` as well as the path and command checks below. `check_env` runs in `handle_env_set` (`POST /env`), in `enforce_exec_policy` (the `env` object on `POST /shell`, `POST /shell/stream`, and `POST /process/start`), and in `_handle_create_session` (`POST /pty/sessions`). `check_url` runs in `handle_browser_navigate` (`POST /browser/navigate`). `check_process` runs in `enforce_exec_policy` (argv for those shell and process routes), in `handle_code_run` (`POST /code/run`), and in `_handle_create_session` (the PTY shell). A denial is HTTP 403 with the guard's code: `env_var_denied`, `url_denied`, or `process_denied`. An empty env map stays allowed. -## What a template declares - -Optional `server:` in `template.yaml`. Absent means the default policy: every path an endpoint takes stays under `ABX_SERVER_BASE_DIR` (default `/home/user`), including the `cwd` of `/shell` and process start, and shell commands are not filtered. Tooling compiles the block to `/etc/agent-sandbox/server-policy.json`. The process also loads `ABX_SERVER_POLICY` (inline JSON) or `ABX_SERVER_POLICY_FILE` when either is set. `agent-sandbox-server check-config --policy ` validates the document and prints lint warnings. An invalid document prevents startup. - -```yaml -server: - preset: transfer-only - base_dir: /home/user - groups: - process: false - terminal: false - endpoints: - files_download_stream: - paths: - allow: [/home/user/out] - deny: ["**/.env", "*.pem"] - read_only: false - symlinks: within - limits: - max_bytes: 5MB - files_upload_stream: - paths: - allow: [/home/user/in] - params: - overwrite: false - deny_extensions: [".sh", ".so"] - shell: - params: - allow_commands: ["ls", "cat"] - deny_commands: ["rm"] - limits: - timeout_seconds: 30 - max_output_bytes: 1048576 +These optional policy settings apply to templates running SandboxServer. The Managed Agents API bridge has its own fixed route contract. + +## Server policy file + +Save a standalone `server-policy.json` and load it with `ABX_SERVER_POLICY_FILE` or `agent-sandbox-server serve --policy server-policy.json`. Inline JSON uses `ABX_SERVER_POLICY`. Without a policy, every endpoint path stays under `ABX_SERVER_BASE_DIR` (default `/home/user`), including shell/process `cwd`; shell commands are not filtered. `agent-sandbox-server check-config --policy server-policy.json` validates the document and prints lint warnings. An invalid document prevents startup. YAML policy files (`.yaml`/`.yml`) also work when PyYAML is installed; this runtime policy is separate from the v1 resource manifest. + +```json +{ + "preset": "transfer-only", + "base_dir": "/home/user", + "groups": { + "process": false, + "terminal": false + }, + "endpoints": { + "files_download_stream": { + "paths": { + "allow": [ + "/home/user/out" + ], + "deny": [ + "**/.env", + "*.pem" + ], + "read_only": false, + "symlinks": "within" + }, + "limits": { + "max_bytes": "5MB" + } + }, + "files_upload_stream": { + "paths": { + "allow": [ + "/home/user/in" + ] + }, + "params": { + "overwrite": false, + "deny_extensions": [ + ".sh", + ".so" + ] + } + }, + "shell": { + "params": { + "allow_commands": [ + "ls", + "cat" + ], + "deny_commands": [ + "rm" + ] + }, + "limits": { + "timeout_seconds": 30, + "max_output_bytes": 1048576 + } + } + } +} ``` +To include the policy in a source image, add this instruction to its Dockerfile: + +```dockerfile +COPY server-policy.json /etc/agent-sandbox/server-policy.json +``` + +The server reads this default path when no explicit policy source is supplied. + Presets, applied before your endpoint rules: `default` (no extra restriction), `transfer-only` (upload and download endpoints only), `read-only` (process, terminal, dev tools, and browser off; write endpoints off), `hardened` (those groups off, plus deny globs on file routes), `locked` (only `list_commands` and `run_command`). `paths.allow`, when present, replaces `base_dir` as the boundary for that endpoint. `paths.deny` is a directory prefix or a glob; a pattern with no `/` matches a path component (`*.pem`, `.env`). Deny wins. `paths.read_only: true` rejects write and delete on that endpoint. `paths.symlinks` is `within` (a symlink is allowed when the target stays inside the boundary) or `never`. diff --git a/docs/en/guide/fine-grained-capabilities.md b/docs/en/guide/fine-grained-capabilities.md index 05377e9f..992d0cb2 100644 --- a/docs/en/guide/fine-grained-capabilities.md +++ b/docs/en/guide/fine-grained-capabilities.md @@ -30,35 +30,48 @@ A denial names the rule that denied it. The 404 body carries `code: endpoint_dis Three inputs, one document. An invalid document stops the process. It does not fall open. -The `server:` block in `template.yaml`. Tooling compiles it to `/etc/agent-sandbox/server-policy.json`. Accepted keys: `version`, `preset`, `base_dir`, `groups`, `defaults`, `auth`, `endpoints`, `description`. `groups` is either a list of groups to turn on or a map of group to `true`/`false`. - -```yaml -server: - endpoints: - "*": - enabled: false - agents_sessions_create: - enabled: true - agents_sessions_events_create: - enabled: true - agents_sessions_items_list: - enabled: true - agents_runtimes_list: - enabled: true +Write a standalone `server-policy.json`, separate from `template.yaml`; copy it into the image and select it at server startup. JSON always works; `.yaml`/`.yml` policy files also work when PyYAML is installed. Accepted keys: `version`, `preset`, `base_dir`, `groups`, `defaults`, `auth`, `endpoints`, `description`. `groups` is either a list of groups to turn on or a map of group to `true`/`false`. + +```json +{ + "endpoints": { + "*": { + "enabled": false + }, + "agents_sessions_create": { + "enabled": true + }, + "agents_sessions_events_create": { + "enabled": true + }, + "agents_sessions_items_list": { + "enabled": true + }, + "agents_runtimes_list": { + "enabled": true + } + } +} ``` Group shorthand, list or map: -```yaml -server: - groups: [agents, dev_tools] # same as {agents: true, dev_tools: true} +```json +{ + "groups": [ + "agents", + "dev_tools" + ] +} ``` -```yaml -server: - groups: - process: false - terminal: false +```json +{ + "groups": { + "process": false, + "terminal": false + } +} ``` Inline JSON: @@ -72,6 +85,7 @@ Or a file: ```bash export ABX_SERVER_POLICY_FILE=/etc/agent-sandbox/server-policy.json agent-sandbox-server check-config --policy /etc/agent-sandbox/server-policy.json +agent-sandbox-server serve --policy /etc/agent-sandbox/server-policy.json ``` Lookup order: `ABX_SERVER_POLICY`, then `ABX_SERVER_POLICY_FILE`, then the well-known policy file. With none of them the built-in defaults apply: opt-in groups off, everything else on, no per-endpoint limits. @@ -284,4 +298,4 @@ This is the endpoint declaration table (`agent_sandbox.server.endpoints.ENDPOINT | `agents_sessions_items_list` | `GET /v1/agents/sessions/{id}/items` | | `agents_runtimes_list` | `GET /v1/agents/runtimes` | -The local e2e recorded `POST /shell` as 404, `POST /v1/agents` (definitions) as 404, and the Codex and Qwen Code session, event, and item calls as 200 / 202. To open another route, add its endpoint id to `ServerPolicy.only` and to `endpoints` in `template.yaml`. +The local e2e recorded `POST /shell` as 404, `POST /v1/agents` (definitions) as 404, and the Codex and Qwen Code session, event, and item calls as 200 / 202. To open another route, add its endpoint id to `ServerPolicy.only` or to `endpoints` in the loaded policy document. diff --git a/docs/en/guide/managed-agents-api.md b/docs/en/guide/managed-agents-api.md index 964e2490..c4b9c086 100644 --- a/docs/en/guide/managed-agents-api.md +++ b/docs/en/guide/managed-agents-api.md @@ -48,6 +48,8 @@ with httpx.Client( print(response.headers["X-Agent-Sandbox-Submission-Id"]) ``` +Session `created_at` and `last_active_at` are Unix timestamps in integer seconds. `required_actions` and `vault_ids` are empty arrays; client-required actions and Vault bindings are unsupported. + Creation returns `201`. Input reception returns a durable Submission, while a public Turn appears when execution starts. Poll the Session and its `/turns` and `/artifacts` collections. Download a published artifact through `/v1/agents/sessions/{id}/artifacts/{artifact_id}/content`. @@ -143,6 +145,14 @@ Storage, queues, execution time, events and subscribers have finite deployment b rejection returns `429`. Expired results return `410`; deleted resources return `404`. Cleanup preserves private ownership records required to resolve uncertain resources. +## Sandbox instance configuration + +The v1 `sandbox.instance` controls each Session's new Sandbox: `timeout`, CPU/memory/disk/GPU, existing Volume-name mounts and user metadata. Managed TTL defaults to3600 seconds; internal lifetime budgets are derived from this single value. New revisions preserve each existing Session's settings and Worker routing. Preparation `sandbox.template.props` does not change a Session instance. + +All custom environment values are private, including values that look harmless. The deployer sends names in compiled configuration and resolves values only for the Worker through `ABX_MANAGED_SANDBOX_ENV_JSON` or `_FILE`; values never enter Session settings or public plans. Worker source names must exactly match `instanceEnvNames`. Model credentials stay in the separate model binding. Runtime/model controls (`ABX_*`, `OPENAI_*`, `QWEN_*`, `CODEX_*`, `PYTHON*`, `LD_*`, `HOME`, `PATH`, `SHELL`, `USER`, `LOGNAME`, `ANTHROPIC_API_KEY`, `DASHSCOPE_API_KEY`) cannot be overridden through custom environment input. + +Managed Sandboxes use secure access and kill-on-timeout without automatic resume. Conflicting lifecycle/unsupported security fields fail before creation. Metadata cannot replace `abx-managed`, `abx-session` or `abx-revision`. Volume mounts cannot overlap the fixed workspace or private runtime state, including their ancestors and descendants. Existing Volumes are borrowed and are never deleted with a Session. This mount interface does not express read-only access: data mounts can be shared and writable, so use appropriately isolated datasets/volumes for different owners. Exclusive Session workspaces do not make an explicitly shared data volume private. + ## Operator configuration Run `python -m agent_sandbox.managed_agents gateway|worker|dispatcher`. The guest image runs @@ -152,8 +162,9 @@ Each configuration document accepts exactly one `_FILE` or `_JSON` source: | Variable prefix | Document | | --- | --- | -| `ABX_MANAGED_CONFIG` | Compiled revision, application ID, template, model/adapter allowlist, `modelEnvAllowlist`, Session/storage/execution budgets, `expectedStateId` and retained Worker mapping | +| `ABX_MANAGED_CONFIG` | Non-secret `instance`, `instanceEnvNames`, compiled revision, application ID, template, model/adapter allowlist, `modelEnvAllowlist`, Session/storage/execution budgets, `expectedStateId` and retained Worker mapping | | `ABX_MANAGED_AUTH` | `tokens: [{sha256,owner,scopes,expiresAt?}]`; scopes are `agents:read` and `agents:write` | +| `ABX_MANAGED_SANDBOX_ENV` | Worker-only custom environment string map, with names exactly matching `instanceEnvNames` | | `ABX_MANAGED_MODEL_CONFIG` | Worker-only `{apiKey,baseUrl,model,adapter}` | | `ABX_MANAGED_PROVIDER_CONFIG` | Worker/Dispatcher `{provider:"alicloud-express",apiKey,baseUrl,domain,region}` | diff --git a/docs/en/guide/sdk-usage.md b/docs/en/guide/sdk-usage.md index 9ae14084..9a940fb8 100644 --- a/docs/en/guide/sdk-usage.md +++ b/docs/en/guide/sdk-usage.md @@ -261,7 +261,7 @@ async for event in watcher: The `sandbox.network` submodule computes port access URLs (purely local computation, no network requests involved). -> **Capability requirement**: Requires the `ports` capability. The default capability set does not include `ports`; it must be explicitly declared in the template's `template.yaml`. +> Port addressing is supplied by the selected Provider. Declare template endpoints with v1 `sandbox.services` or additional `sandbox.ports`; a declaration does not grant Provider operations. The template launcher requires the selected Provider's `ports.resolve` support for declared endpoints. ```python # Get port URL @@ -415,8 +415,7 @@ policy = { SandboxServer(policy=policy).serve(9000) # a dict or a ServerPolicy ``` -or declaratively in `template.yaml` (`server:` block, see the [template spec](../reference/template-yaml-spec.md)), or -`agent-sandbox-server serve --policy server-policy.json`. Validate with `agent-sandbox-server check-config`. +Alternatively, save the policy as `server-policy.json` and run `agent-sandbox-server serve --policy server-policy.json`, or set `ABX_SERVER_POLICY_FILE` before starting your `commands.py`. Validate it with `agent-sandbox-server check-config --policy server-policy.json`. See [policy loading](fine-grained-capabilities.md#how-to-turn-it-on). ### Handling denials diff --git a/docs/en/guide/template-server-api.md b/docs/en/guide/template-server-api.md index 75e4cbc7..1b78f0a9 100644 --- a/docs/en/guide/template-server-api.md +++ b/docs/en/guide/template-server-api.md @@ -99,10 +99,10 @@ Ways to switch, all ending in the same per-endpoint rules: | Where | Form | |---|---| | `commands.py` | `default_table().enable_group(CapabilityGroup.FILE_OPS)` / `disable_group(...)` | -| `template.yaml` `server:` | `groups: [agents, dev_tools]` or `groups: {process: false}`; `endpoints:` for single ids | +| standalone policy document | `groups: [agents, dev_tools]` or `groups: {process: false}`; `endpoints:` for single ids | | environment | `ABX_SERVER_ENABLED_GROUPS=agents,dev_tools` (comma list, turns groups on only) | -There is no environment variable that turns a group off. Write `groups: {process: false}` in `server:` or an `endpoints` rule instead. Layers, specificity and the `rule` names are in [Fine-grained capabilities](fine-grained-capabilities.md). +There is no environment variable that turns a group off. Write `groups: {process: false}` in the loaded policy document or use an `endpoints` rule instead. Layers, specificity and the `rule` names are in [Fine-grained capabilities](fine-grained-capabilities.md). `provides`, explicit workflows, exact provider operations and this endpoint policy are separate contracts. Retired template `capabilities` is rejected; a workflow never enables an endpoint automatically. @@ -626,7 +626,7 @@ All of these need Playwright. If `import playwright` fails the handler returns ` ### `agents` (off until enabled) -Turn the group on with `ABX_SERVER_ENABLED_GROUPS=agents`, `groups: [agents]` in `server:`, or `default_table().enable_group(CapabilityGroup.AGENTS)` before `serve()`. All 70 routes are implemented. Two runtimes have a local backend: Codex CLI (`codex`) and Qwen Code (`qwen-code`, also `qwen` and `qwencode`). `claude-code`, `cursor` and `command` are declared adapters without a local backend. `GET /v1/agents/runtimes` lists every declared runtime with `implemented` and `installed`. Selecting a runtime that is not implemented is `404` `not_found_error` (`agents.not_implemented_locally`) with `x-should-retry: false`, and the body points to `GET /v1/agents/runtimes`. When the body and `agents.default` (`ABX_AGENTS_DEFAULT`) do not name an adapter, create calls use `codex`. Each Agents route can be enabled on its own. Endpoint ids are in [Fine-grained capabilities](fine-grained-capabilities.md). The default scope is `agents:use`. The full route table is in the [Agents API reference](../reference/agents-api.md). +Turn the group on with `ABX_SERVER_ENABLED_GROUPS=agents`, `groups: [agents]` in the loaded policy, or `default_table().enable_group(CapabilityGroup.AGENTS)` before `serve()`. All 70 routes are implemented. Two runtimes have a local backend: Codex CLI (`codex`) and Qwen Code (`qwen-code`, also `qwen` and `qwencode`). `claude-code`, `cursor` and `command` are declared adapters without a local backend. `GET /v1/agents/runtimes` lists every declared runtime with `implemented` and `installed`. Selecting a runtime that is not implemented is `404` `not_found_error` (`agents.not_implemented_locally`) with `x-should-retry: false`, and the body points to `GET /v1/agents/runtimes`. When the body and `agents.default` (`ABX_AGENTS_DEFAULT`) do not name an adapter, create calls use `codex`. Each Agents route can be enabled on its own. Endpoint ids are in [Fine-grained capabilities](fine-grained-capabilities.md). The default scope is `agents:use`. The full route table is in the [Agents API reference](../reference/agents-api.md). Every response carries `X-Agent-Sandbox-Local: 1` and every object an `x_agent_sandbox` label (`local`, `compute`, `differences`). A field the server cannot honor is `400` with `param`, never accepted and ignored. Objects belong to the caller; a foreign id answers like an unknown id. @@ -755,7 +755,7 @@ curl --fail-with-body -sS "${AUTH[@]}" \ "$ABX_TEMPLATE_URL/commands/hello" ``` -Upload and download. The path must stay under `ABX_SERVER_BASE_DIR` (default `/home/user`). `file_ops` must be enabled, which it is unless a `server:` policy or `commands.py` turned it off. +Upload and download. The path must stay under `ABX_SERVER_BASE_DIR` (default `/home/user`). `file_ops` must be enabled, which it is unless a loaded server policy or `commands.py` turned it off. ```bash B64=$(printf 'hello' | base64) diff --git a/docs/en/guide/template-workflows.md b/docs/en/guide/template-workflows.md index dfb90297..7a2a44c0 100644 --- a/docs/en/guide/template-workflows.md +++ b/docs/en/guide/template-workflows.md @@ -21,7 +21,7 @@ finally: The client also sends the explicit application token in the server's existing `X-Access-Token` header when that header is unused. This keeps authentication working through proxies that strip `Authorization`, including OpenSandbox 1.1.1. Existing gateway headers, including defaults on an injected HTTP client, are never replaced. Bearer authentication remains available for services that require it. -For advanced preparation, `launch_template` creates a new sandbox from a prepared provider reference and runs the manifest's `start_cmd`. The reference must not also start the same service through its native startup command or image entrypoint. CLI build artifacts retain their declared startup behavior; do not treat a prepared reference as an instruction to start the service twice. If the CLI has already started the application, connect to its sandbox as above. +For advanced preparation, `launch_template` creates a new sandbox from a prepared provider reference and runs the manifest's `sandbox.start`. The reference must not also start the same service through its native startup command or image entrypoint. CLI build artifacts retain their declared startup behavior; do not treat a prepared reference as an instruction to start the service twice. If the CLI has already started the application, connect to its sandbox as above. To let Python own startup and cleanup after checking that preparation boundary: @@ -35,8 +35,8 @@ async with await launch_template( result = await session.run_command("hello", name="Python") ``` -There is no workflow selection. Python creates one sandbox, runs `start_cmd` once, -checks `ready_cmd`, and resolves every service in `provides`. SandboxServer protocol v2 services +There is no workflow selection. Python creates one sandbox, runs `sandbox.start` once, +checks `sandbox.ready`, and resolves every service in `sandbox.services`. SandboxServer protocol v2 services receive health/discovery checks before use. Other service protocols use the template's readiness command; their endpoints are returned without guessing protocol routes. `session.services` contains every service client, while `session.servers` contains @@ -47,7 +47,7 @@ Choosing a client never changes which template services were started. Python does not build sources. Source preparation belongs to the CLI; choose one startup owner as described above. A manifest may -also declare a fixed digest-pinned `image`, or `provider` plus `template_id` for a +also declare a fixed digest-pinned `sandbox.template.image`, or a `sandbox.template.reference` and `sandbox.provider` for a platform-managed template. References are interpreted by that Provider; native IDs need no artificial version. For an existing external HTTP service, use `ServerClient` directly. Application deployment hooks still belong to the deployment orchestrator. @@ -59,16 +59,15 @@ failure of a later service. Journals exclude tokens, environment values and sign they do not constitute verified cloud receipts. One effective `ABX_SERVER_TOKEN` value is used both for the sandbox environment and the application clients. -After process-start acknowledgement, the launcher polls a nonzero `ready_cmd` result within one `readiness_timeout` budget. Each probe uses the remaining budget; a successful exit completes readiness. Expiration raises `readiness-command-failed` and removes the owned sandbox. Cancellation also retains owned cleanup. This waits for application readiness, which is separate from acknowledging that its process started. +After process-start acknowledgement, the launcher polls a nonzero `sandbox.ready` result within one `readiness_timeout` budget. Each probe uses the remaining budget; a successful exit completes readiness. Expiration raises `readiness-command-failed` and removes the owned sandbox. Cancellation also retains owned cleanup. This waits for application readiness, which is separate from acknowledging that its process started. -## Migrating specification 4 +## Resource configuration -Use `spec_version: 5`; remove `workflow_contract_version`, `default_workflow`, and -`workflows`. Retain the full `provides` map and the template's actual `start_cmd` and -`ready_cmd`. Remove CLI `--workflow` and replace Python `launch_workflow` with -`launch_template` without `workflow_id`. Old manifests fail with a migration error; -they are not silently reduced to their former default. Historical specification 4 -receipts remain history and do not prove the specification 5 startup path. +The v1 examples describe the updated SDK source consumer. Use a matching [source installation](install-python.md) when this behavior is not in your installed distribution; unchanged package version metadata alone does not establish v1 support. + +Use the [v1 resource fields](../reference/template-yaml-spec.md). Parsing literal prepared input needs no CLI installation; the selected Provider can still have its own runtime dependencies. Native expressions must first be resolved by the Devs parser. `sandbox.instance.timeout` supplies the TTL unless the explicit Python `timeout` argument overrides it; omitted overrides preserve the manifest. Instance specs, existing Volume mounts, environment and metadata are forwarded. Environment values never implicitly read the host environment. Application deployment remains an orchestrator responsibility and is rejected before creating a sandbox. + +Additional declared `sandbox.ports` are returned as `session.endpoints[port]` Provider descriptors, without creating clients or probing them. Their gateway headers stay in memory and are not part of the public plan or journal. A port declaration does not establish native TCP reachability or start that service. ## Interpreter operations @@ -91,10 +90,10 @@ finally: await sandbox.kill() # this code created and owns the sandbox ``` -With a specification 5 manifest containing `provider: alicloud-express`, -`template_id: base` and `provides: {}`, `launch_template(manifest, -provider="alicloud-express")` offers the same owned cleanup through its async context -manager. No workflow declaration or selector is required. +With `specVersion: 1`, `sandbox.provider: alicloud-express`, +`sandbox.template.reference: base` and no services, `launch_template(manifest, +provider="alicloud-express")` provides the same owned cleanup through its async +context manager. Existing Volume mounts are borrowed; Sandbox cleanup never deletes them. Executing a shell command on `code-interpreter-v1` does not exercise the interpreter API. Express, E2B and REST project adapters currently do not implement unified interpreter operations; use the documented native SDK where applicable. CubeSandbox and container edition support the persistent default-context operation. diff --git a/docs/en/guide/troubleshooting.md b/docs/en/guide/troubleshooting.md index 8c60d3d5..712d4368 100644 --- a/docs/en/guide/troubleshooting.md +++ b/docs/en/guide/troubleshooting.md @@ -124,11 +124,11 @@ Template software, the project provider adapter, the selected client and the cur ### Solution -1. Identify the workflow, consumer and owner of the failed condition. Use a [spec4 manifest](../reference/template-yaml-spec.md); adding retired `capabilities` or `tested_on` is invalid. +1. Identify the workflow, consumer and owner of the failed condition. Use a [v1 resource manifest](../reference/template-yaml-spec.md); adding retired `capabilities` or `tested_on` is invalid. 2. Implement/install the missing adapter operation or explicitly choose an implemented workflow. Stateless, default persistent context, explicit contexts and realtime streams are different operations. 3. For an application, resolve the endpoint independently with `async with await sandbox.application_client(9000, token=application_token)`. Check the listening process, protocol version, gateway configuration and application token. 4. HTTP 401 never retries anonymously; interpreter HTTP 404 never becomes shell execution. Use the commands API explicitly when shell is intended. -5. Server policy separately controls nine endpoint groups. `core` stays on; `dev_tools`, `browser` and `agents` start off. Check the actual loaded policy, `server:` and environment, not retired template flags. +5. Server policy separately controls nine endpoint groups. `core` stays on; `dev_tools`, `browser` and `agents` start off. Check `ABX_SERVER_POLICY`, `ABX_SERVER_POLICY_FILE`, the actual loaded policy and environment, not retired template flags. See [explicit template workflows](template-workflows.md) and [Template server HTTP API](template-server-api.md). diff --git a/docs/en/reference/api-reference.md b/docs/en/reference/api-reference.md index 68387df9..be531040 100644 --- a/docs/en/reference/api-reference.md +++ b/docs/en/reference/api-reference.md @@ -368,6 +368,7 @@ you want policy-scoped, scope-checked access, or need endpoints envd lacks (`fil | Member | Signature | Description | |--------|-----------|-------------| | `ServerClient()` | `(base_url, *, token=None, headers=None, pty_url=None, timeout=30.0, http_client=None, expires_at=None)` | `token` is sent as `Authorization: Bearer`; `headers` carry the provider gateway credentials | +| `ProviderSandbox.application_endpoint()` | `async (port=9000, *, traffic_access_token=None) -> dict[str, Any]` | Resolves the Provider descriptor and gateway headers without creating a client or probing readiness; does not promise native TCP access | | `ProviderSandbox.application_client()` | `async (port=9000, *, token=None, traffic_access_token=None, timeout=30.0) -> ServerClient` | Resolves the provider application gateway descriptor, preserves its prefix/query and gateway headers, and uses a separate application token. Recommended across providers | | `Sandbox.server` | `cached_property -> ServerClient` | Client for port 9000. Native local URL helper; use application_client for a provider gateway descriptor | | `Sandbox.server_at()` | `(port=9000, *, token=None, pty_port=9001, timeout=30.0) -> ServerClient` | A fresh client. Native local URL helper; use application_client for a provider gateway descriptor; `token` is `ABX_SERVER_TOKEN` | @@ -513,3 +514,5 @@ See [Error Codes Reference](./error-codes.md) for the complete error code listin `ServerClient.commands.invoke(name, arguments, /, *, request_timeout=None)` and `ProviderSandbox.invoke(name, arguments, /, *, server_port=9000, application_token=None, request_timeout=None)` send `arguments` as the command JSON body. Both expose `invoke_sync`; legacy `run` keeps its keyword contract. Set `application_token` to the application's `ABX_SERVER_TOKEN` when its service requires authentication. Provider lifecycle methods that accept cloud AccessKeys also accept `security_token`; the environment name is `ALIBABA_CLOUD_SECURITY_TOKEN`. `ProviderSandbox.create` additionally accepts `network` and `allow_internet_access`. `ProviderSandbox.connect(..., traffic_access_token=...)` retains GET-only attach semantics, and `application_client(..., traffic_access_token=...)` provides a per-client gateway credential override. See [provider commands](../guide/provider-commands.md) and [authentication](../guide/authentication.md). + +For canonical v1 prepared templates, `launch_template(..., timeout=None)` preserves the declared instance TTL. Explicit resource and environment arguments override the corresponding values. `TemplateSession.services` holds actual declared service clients; `TemplateSession.endpoints` holds additional port descriptors with headers only in memory. See [template fields](template-yaml-spec.md) and [launch ownership/cleanup](../guide/template-workflows.md). diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 18e99a76..d6f4d906 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -117,8 +117,8 @@ The following environment variables are used for in-sandbox Server configuration | `ABX_SERVER_TOKEN` | — | Server authentication token | | `ABX_SERVER_BASE_DIR` | `/home/user` | Directory every path-taking endpoint (including shell `cwd`) must stay under | | `ABX_SERVER_ENABLED_GROUPS` | — | Comma list of groups to turn on, for example `agents,dev_tools`. The only group variable; there is none for turning groups off | -| `ABX_SERVER_POLICY` | — | Inline JSON policy document (same keys as the `server:` block) | -| `ABX_SERVER_POLICY_FILE` | — | Path to a policy JSON file | +| `ABX_SERVER_POLICY` | — | Inline standalone JSON policy document | +| `ABX_SERVER_POLICY_FILE` | — | Path to a policy JSON file, or `.yaml`/`.yml` when PyYAML is installed | | `ABX_PTY_PORT` | `9001` | PTY terminal WebSocket port | | `ABX_MAX_UPLOAD_SIZE` | 100 MiB | Maximum decoded upload size | diff --git a/docs/en/reference/error-codes.md b/docs/en/reference/error-codes.md index 97a9d350..f4e794bc 100644 --- a/docs/en/reference/error-codes.md +++ b/docs/en/reference/error-codes.md @@ -44,7 +44,7 @@ except SandboxError as e: | E2001 | `TemplateNotFoundError` | Template does not exist | Run `abx template list` to view available templates | Specified a non-existent template name | | E2002 | `QuotaExceededError` | Quota exceeded | Destroy idle sandboxes or request a quota increase | Number of running sandboxes exceeds account quota | | E2003 | `RegionUnavailableError` | Region unavailable | Try another region or check service availability | Requested region is temporarily unavailable or does not exist | -| E2004 | `TemplateParseError` | Manifest parse failure | Fix spec4, required workflow fields and unknown/retired fields | Current YAML violates the contract; no default permissions allow it to continue | +| E2004 | `TemplateParseError` | Manifest parse failure | Fix specVersion: 1, required workflow fields and unknown/retired fields | Current YAML violates the contract; no default permissions allow it to continue | | E2009 | `SandboxNotFoundError` | No sandbox with this ID exists | Run `abx list` to see the running sandboxes; a killed or expired sandbox cannot be reconnected | `Sandbox.connect()` / `get_info()` / `set_timeout()` / `pause()` / `resume()` on an ID the platform answers with HTTP 404 (`kill` stays idempotent) | ### Troubleshooting Steps diff --git a/docs/en/reference/template-yaml-spec.md b/docs/en/reference/template-yaml-spec.md index 5e13dfa0..c8164987 100644 --- a/docs/en/reference/template-yaml-spec.md +++ b/docs/en/reference/template-yaml-spec.md @@ -1,122 +1,68 @@ # template.yaml Specification -Current templates use `spec_version: 5`. The complete publishing authority is [awesome-templates/template-v5.schema.json](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v5.schema.json); the shared launch contract lives in [contracts](https://github.com/Alibaba-Agent-Sandbox/python-sdk/tree/main/contracts). The full schema retains application deployment topology, hooks and durable state. +Use `specVersion: 1` and one local `template.yaml`. The complete field types and Provider subsets belong to the [template v1 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v1.schema.json) and [template specification](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/docs/template-spec.md). Publication metadata and initialization questions belong to optional `registry.yaml`; local runs do not require it or `parameters`. -## File Location +Template files use YAML1.2 Core with strict UTF-8 and a 1 MiB limit. Plain `on` and dates such as `2026-11-12` remain strings. Duplicate keys, aliases, unsupported tags, non-string mapping keys and non-finite/non-JSON values are rejected before resource creation. -Each source template has one `template.yaml` at its root. Source catalog rows locate that file rather than repeating workflows; managed entries embed the entire declaration. Dockerfiles, command modules and dependency files form the source input alongside the manifest. +| Field | Meaning | +| --- | --- | +| `specVersion` | Integer `1` | +| `name` | Resource name | +| `sandbox.provider` | Selected Provider ID; Python's explicit Provider context can fill it | +| `sandbox.template` | Exactly one `source`, `image` or `reference`; optional preparation `props` and `aliases` | +| `sandbox.instance` | Runtime TTL, environment, metadata and Provider-supported instance settings | +| `sandbox.start`, `sandbox.ready` | Whole-sandbox startup and readiness commands | +| `sandbox.ports` | Optional additional port numbers; no inferred protocol or automatic service start | +| `sandbox.services` | Endpoint map: protocol, integer version, port and optional operations | +| `deployment` | Native Serverless Devs project; `resources.*.component` and native `props` | +| `application` | Explicit supported application coordinator, currently `type: agents-api` | -## Complete Field Definitions +Preparation properties under `sandbox.template.props` do not become instance overrides. Existing template references cannot take preparation properties. Python consumes instance CPU/memory/disk/GPU, existing Volume-name mounts, environment, metadata and TTL when supported by the selected schema and Provider. It accepts the fixed `lifecycle: {onTimeout: kill, autoResume: false}` policy; pause/resume lifecycle orchestration is unavailable in this launcher and fails before creation. The independent `ProviderSandbox` lifecycle APIs retain their own contract. -### Basic Information +Generic instance TTL defaults to 300 seconds; Managed Agents API uses 3600. Explicit Python `timeout`, `cpu`, `memory`, `disk`, `gpu` and `volume_mounts` replace the corresponding instance values. Explicit `envs` and `metadata` merge by name; the launch ownership metadata is reserved. Memory/disk units and availability follow the Provider, not a universal cloud quota promise. Express's high-level schema uses integer MB values. -The publishing schema requires metadata including `name`, `version`, `description`, `author` and `license`; consult it for `tags`, `priority`, `wave` and `smoke`. The SDK runtime model does not replace the complete publishing schema. - -### Runtime Configuration - -`env` is a string map and `ports` lists application ports. `start_cmd` and `ready_cmd` participate in explicit startup behavior. The actual service must listen on the declared port. Parsing SDK metadata never rewrites a Dockerfile. - -### Resource Specifications - -Published source manifests use `resources.cpu` and `resources.memory`; the SDK runtime configuration can also carry `cpu_count` and `memory_mb`. Application resources and topology follow the full schema. Planning a sandbox workflow does not execute an application deployment. - -### Sandbox Generation - -`generation` belongs to the selected template/provider startup configuration; the catalog source schema currently requires `2`. Creation still requires the provider to implement the declared input and startup mode. +Native `${...}` expressions require the real Devs parser. Python's prepared launcher accepts literals or an already resolved v1 snapshot and explicitly rejects unresolved expressions. Environment values are literal; empty strings do not implicitly read the host environment. Resolve secrets in the caller and pass them through `envs` or the resolved snapshot. Python does not construct source images or turn arbitrary component properties into cloud support. Custom Providers without actual v1 validators are refused by the prepared launcher; their independent Provider APIs remain available. -### Template startup - -`spec_version` is integer `5`. `provides` describes every service through `protocol`, -`version`, `port` and `operations`, and may be empty. `start_cmd` starts the complete -template once; when absent, the image or platform template supplies its own startup. -`ready_cmd`, when present, is executed before service clients are returned. A fixed -`image` or `provider` plus `template_id` can identify a prepared template; otherwise -the CLI prepares its source. Python requires a prepared reference. - -Remove `workflow_contract_version`, `default_workflow` and `workflows` when migrating -from specification 4. No application/exec/interpreter type or operation is selected -at launch. Retired `capabilities`, `tested_on` and provider compatibility tables are -still rejected. Actual Provider and server capabilities are checked at invocation. -See [launching templates](../guide/template-workflows.md). +## Services and server policy -### Removed key: `custom_commands` +`sandbox.services` describes endpoints; it does not enable server APIs or grant Provider operations. The launcher starts the entire template once. SandboxServer protocol v2 services receive health/discovery checks. Other protocols use `sandbox.ready` and are returned without inferred routes. -`custom_commands` is rejected. Register named functions with `@command` in the server's `commands.py` and invoke them through application HTTP; YAML does not generate functions. +The root `server` key is not a v1 resource field. SandboxServer endpoint policy remains its own runtime configuration, loaded through the documented policy file or `ABX_SERVER_POLICY_FILE` / `ABX_SERVER_POLICY`. `validate_server_block` remains a standalone structural validator; the real server validates policy values. See [fine-grained capabilities](../design/fine-grained-capabilities.md). Named commands are Python functions registered with `@command`; YAML does not create them. -### Server Policy (`server`) +## Prepared reference example -Optional. Declares which SandboxServer endpoints the template exposes and under which restrictions -(design: [fine-grained-capabilities](../design/fine-grained-capabilities.md)). Absent = the server's default -behaviour. The block is validated structurally by `SandboxTemplate` (unknown keys are errors); values are -validated by `agent-sandbox-server check-config`. The server loads `/etc/agent-sandbox/server-policy.json` or the explicit `ABX_SERVER_POLICY_FILE`/`ABX_SERVER_POLICY` input. Ensure the chosen build/startup flow actually installs that policy; declaring YAML alone does not apply it. - -| Key | Type | Description | -|-----|------|-------------| -| `preset` | `str` | `default`, `transfer-only`, `read-only`, `hardened`, `locked` | -| `base_dir` | `str` | Root for relative paths and the default path boundary | -| `groups` | `list[str]` or `map[str, bool]` | Turn capability groups on (`[agents, dev_tools]`) or on/off (`{process: false}`). A group is an alias for its endpoint ids | -| `defaults` | rule | Rule applied to every endpoint before its own rule | -| `endpoints` | `map[str, rule]` | Per-endpoint rules keyed by endpoint id (`files_download_stream`, `shell`, ...) | -| `auth` | map | `tokens` (`id`, `scopes`, `token_env` or `token_sha256`), `allow_anonymous`, `anonymous_scopes` | - -A rule has `enabled`, `scopes`, `paths` (`allow`, `deny`, `read_only`, `symlinks`), `limits` (`max_bytes`, `max_request_bytes`, `timeout_seconds`, `rate_per_minute`, ...), `params` (`overwrite`, `deny_extensions`, `allow_commands`, ...) and `audit`. +The referenced template must already contain the application and server dependencies. The existing Volume must be accessible to the selected Provider/account. Python creates and owns only the Sandbox; it never deletes the borrowed Volume. ```yaml -server: - preset: transfer-only - endpoints: - files_download_stream: - paths: { allow: [/home/user/out] } - limits: { max_bytes: 5MB } - files_upload_stream: - paths: { allow: [/home/user/in] } - params: { overwrite: false } +specVersion: 1 +name: python-hello +sandbox: + provider: alicloud-express + template: + reference: base + instance: + timeout: 600 + cpu: 2 + memory: 2048 + envs: + APP_MESSAGE: hello + metadata: + purpose: example + volume_mounts: + /data: existing-volume-name + start: python /home/user/app/commands.py + ready: agent-sandbox-server healthcheck --port 9000 + services: + app: + protocol: agent-sandbox-server + version: 2 + port: 9000 + operations: [commands.list, commands.run] ``` +For source preparation, replace `reference` with `source: {dockerfile: Dockerfile, context: .}` and use the CLI; Python still requires its prepared reference. Choose one startup owner so the image's native entrypoint does not launch the same application twice. See [launch and cleanup](../guide/template-workflows.md). -## Complete Example - -This source-publishing example includes required metadata. Resolve its base-image placeholders through an explicit configuration before building. - -```yaml -name: my-python-template -version: 1.0.1 -description: Scaffold for a new Agent Sandbox template. Not a catalog entry. -author: alibaba-agent-sandbox -license: Apache-2.0 -spec_version: 5 -base: fc-e2b-registry..cr.aliyuncs.com/runtime/base: -generation: 2 -start_cmd: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py -ready_cmd: agent-sandbox-server healthcheck --port 9000 -resources: - cpu: 2 - memory: 2048 -ports: -- 9000 -tags: -- example -- skeleton -priority: P2 -wave: other -env: - ABX_SERVER_TOKEN: '' -smoke: -- command: hello - args: - name: Express - expect: Hello, Express! -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - commands.list - - commands.run -``` - -## Build Notes +Provider authors can override the optional synchronous `Provider.validate_template_input(stage, value)` with their version's actual schema. Stages are `template.prepare` (template object) and `sandbox.create` (instance object). Return `False` or raise to reject. The inherited default is not a validator; asynchronous validation is rejected. Admission does not grant operations or forward instance fields that the high-level creation API has not implemented. -`base` is valid source/template metadata. Builds use the selected effective Dockerfile and explicit build configuration; they never silently replace `FROM`, users, copy paths or entrypoints. Install dependencies through native files in the image. The Python launcher does not prepare source; pass a prepared Provider reference or connect an existing ServerClient. +`sandbox.ports` preserves optional extra ports such as browser debugging or a database port. The compiled port list is the sorted union with service ports. `session.endpoints[port]` returns the Provider descriptor for extra ports outside `sandbox.services`, including gateway headers only in memory. It creates no ServerClient and sends no health/discovery request. A declared port does not guarantee native TCP or public connectivity; use the Provider's actual addressing/protocol. `await sandbox.application_endpoint(port)` provides the same descriptor independently. Existing service clients remain in `session.services`. diff --git a/docs/zh/design/template-system.md b/docs/zh/design/template-system.md index 82cce73e..502c9bb5 100644 --- a/docs/zh/design/template-system.md +++ b/docs/zh/design/template-system.md @@ -1,6 +1,6 @@ # 模板系统 -模板采用规范 5,描述完整沙箱内容、启动与就绪命令,以及所有对外服务。 +模板采用规范 1,描述完整沙箱内容、启动与就绪命令,以及所有对外服务。 启动模板不会再按工作流、单个操作或 application/exec/interpreter 类型选择其中一部分。 ## 职责 @@ -16,15 +16,15 @@ CLI 使用 `abx --provider PROVIDER deploy TEMPLATE_REF`;Python 使用 就绪和清理实际需要的操作。缺少解释器不阻止无关应用启动。执行具体操作时, Provider 和服务端策略仍负责实际能力与权限检查。 -保留 `provides` 中所有服务。SandboxServer 协议 v2 额外检查 health/discovery;其他协议返回客户端, +保留 `sandbox.services` 中所有服务。SandboxServer 协议 v2 额外检查 health/discovery;其他协议返回客户端, 由作者的就绪命令检查。多服务模板只启动一次,后续选择调用地址不会改变启动内容。 ## 清单与迁移 参见[字段参考](../reference/template-yaml-spec.md)和权威的 -[规范 5 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v5.schema.json)。 +[规范 1 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v1.schema.json)。 删除 `workflow_contract_version`、`default_workflow`、`workflows`,保留 -`spec_version: 5`、`provides`、`start_cmd`、`ready_cmd` 以及应用元数据。 +`specVersion: 1`、`sandbox.services`、`sandbox.start`、`sandbox.ready` 以及应用元数据。 旧运行清单明确要求迁移,不静默选择旧默认项。源码位置、镜像引用和平台模板 ID 属于准备过程的输入,不再暴露为用户选择的启动流程。 @@ -34,7 +34,7 @@ Provider 和服务端策略仍负责实际能力与权限检查。 清理时尝试关闭所有客户端并删除自己创建的沙箱,保留失败记录供核对。 连接已有沙箱不代表获得删除它的所有权。 -目录收录或静态计划支持不等于实测成功。历史工作流证据保留,但不认证规范 5。 +目录收录或静态计划支持不等于实测成功。历史工作流证据保留,但不认证规范 1。 模板元数据版本不代表有一个 Registry 版本存储服务器;复现应使用真实 Git 修订和运行输入。 参见 [Python 启动示例](../guide/template-workflows.md)、 diff --git a/docs/zh/guide/authoring-templates.md b/docs/zh/guide/authoring-templates.md index 03d6fa15..63ad7fa8 100644 --- a/docs/zh/guide/authoring-templates.md +++ b/docs/zh/guide/authoring-templates.md @@ -1,160 +1,22 @@ # 编写模板 -当前 SDK 安装使用[安装指南](install-python.md)。 +模板包含应用源码、Dockerfile 和一个 `template.yaml`。使用 `specVersion: 1`;[字段参考](../reference/template-yaml-spec.md)说明资源、实例与原生 Serving 配置。可选的 `registry.yaml` 负责目录描述和初始化问题。依赖写入 Dockerfile 与语言原生依赖文件。 -> **说明:** 本页出现的 `abx` 命令由 `cli` 仓库中的 Node.js CLI 提供([CLI binary installation](https://github.com/Alibaba-Agent-Sandbox/agent-sandbox/blob/main/docs/en/guide/binary-installation.md))。CLI 专属文档(CLI 设计、教程、参考)也在该仓库;本仓库记录 Python SDK(`agent-sandbox-client`)与容器侧运行时(`agent-sandbox-server`)。 - -这是 **Server SDK**:`commands.py` 随镜像启动,在沙箱里面提供 HTTP 服务。在你的机器上创建并调用沙箱见 [Client SDK](client-sdk.md)。 - -本文档介绍如何创建自定义 Agent Sandbox 模板,包括目录结构、字段参考、整体启动、注册命令和发布流程。 - -模板是一个由纯文件组成的目录,可手写也可生成。`abx template init` 命令有三种生成方式(内置脚手架、一句话描述、适配已有源码的项目);该工作流记录在 CLI 仓库:[模板编写(CLI)](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/zh/guide/authoring-templates.md)。 - ---- - -## 模板目录结构 - -一个完整的模板目录结构如下: - -```text -my-template/ -├── template.yaml # 必须 — 模板定义 -├── Dockerfile # 可选 — 自定义 Docker 构建(优先于 template.yaml 中的构建步骤) -├── commands.py # 可选 — 用 @command 注册命名命令 -└── README.md # 可选 — 模板说明 -``` - -`template.yaml` 是唯一必需的文件。 - ---- - -## template.yaml 基础示例 - -```yaml -name: my-python-template -version: 1.0.1 -description: Scaffold for a new Agent Sandbox template. Not a catalog entry. -author: alibaba-agent-sandbox -license: Apache-2.0 -spec_version: 5 -base: fc-e2b-registry..cr.aliyuncs.com/runtime/base: -generation: 2 -start_cmd: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py -ready_cmd: agent-sandbox-server healthcheck --port 9000 -resources: - cpu: 2 - memory: 2048 -ports: -- 9000 -tags: -- example -- skeleton -priority: P2 -wave: other -env: - ABX_SERVER_TOKEN: '' -smoke: -- command: hello - args: - name: Express - expect: Hello, Express! -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - commands.list - - commands.run -``` - ---- - -## 字段参考 - -| 字段 | 类型 | 必须 | 默认值 | 说明 | -|------|------|------|--------|------| -| `name` | `str` | ✅ | — | 模板名称 | -| `version` | `str` | ❌ | `"1.0.0"` | 语义版本号 | -| `description` | `str` | ❌ | `""` | 模板描述 | -| `env` | `dict[str, str]` | ❌ | `{}` | 环境变量 | -| `cpu_count` | `int` | ❌ | `None` | 默认 CPU 核数 | -| `memory_mb` | `int` | ❌ | `None` | 默认内存 MB | -| `ports` | `list[int]` | ❌ | `[]` | 暴露的端口列表 | -| `author` | `str` | ❌ | `""` | 作者 | -| `license` | `str` | ❌ | `""` | 许可证 | -| `tags` | `list[str]` | ❌ | `[]` | 标签 | - -详细的字段定义参见 [template.yaml 规范](../reference/template-yaml-spec.md)。 - ---- - - -## 模板启动 - -使用 `spec_version: 5`,在 `provides` 声明全部服务。`start_cmd` 和 `ready_cmd` -作用于完整模板,不再声明或选择工作流。Provider 提供平台操作,服务端策略控制应用 API, -二者都不推导解释器支持。见[模板启动指南](template-workflows.md)。 - -服务端端点策略保持独立:`core` 始终开启,`dev_tools`、`browser`、`agents` -需要显式配置。模板元数据声明 API 不代表打开对应服务端策略。 - ---- - -## 注册命令 - -命名命令只有一种机制:注册在模板 server 里的函数。`template.yaml` 没有 `custom_commands` 键(客户端会拒绝它)。在 `commands.py` 中用装饰器注册 Python 函数: +Python 应用端点在镜像内安装 `agent-sandbox-server`,注册真实业务函数: ```python from agent_sandbox.server import command, start -@command("demo", description="回显参数。") -def demo(x: int, y: str) -> str: - return f"{y}={x}" +@command("hello", description="Greet a user") +def hello(name: str) -> str: + return f"Hello, {name}!" if __name__ == "__main__": start(9000) ``` -文件上传和下载是 server 的端点(`/files/*`、`/upload`、`/download`),属于默认开启的 `file_ops` 组,不在 `commands.py` 里注册。部署不会应用 `server:` 块。可选组用 `default_table().enable_group(CapabilityGroup.DEV_TOOLS)`(参数是 `CapabilityGroup`,不是字符串)或 `ABX_SERVER_ENABLED_GROUPS` 打开。`dev_tools`、`browser`、`agents` 默认关闭。 - -注册的命令由沙箱内 HTTP server 提供(`POST /commands/{name}`)。客户端用 `await sandbox.run("demo", x=1, y="hello")` 或 `abx sandbox command run demo --arg x=1 --arg y=hello` 调用,用 `sandbox.list_commands()` 或 `abx sandbox command list ` 列出。裸 shell 命令是另一个调用:`sandbox.commands.run(cmd)`。 - ---- - -## Dockerfile 自定义 - -目录里有 `Dockerfile` 时,镜像就是这个文件。`template.yaml` 不安装软件包。`base` 是合法来源元数据,但不会替换 Dockerfile 的 `FROM`;已移除的清单软件包安装字段被拒绝,依赖安装写入有效 Dockerfile 及原生依赖文件。 - -源 Dockerfile 保持原样。含未解析基础镜像占位符时,应修正源输入或显式选择构建配置;不能按 Provider 静默换成另一张镜像。`base` 字段合法,但不替换 Dockerfile 的 `FROM`。 - -## 提供者模板配置 - -服务端不选择 Provider。9000 是应用 HTTP 默认监听端口;平台注入的 envd、解释器及保留端口属于各后端,不能据此推导应用服务不可用。声明的 `start_cmd` 只运行一次,没有该字段时使用镜像或平台模板入口;不要让原生入口与启动命令重复运行。非空 `ready_cmd` 必须实际检查。构建配置必须明确选择,其有效 Dockerfile 和构建参数进入输入身份。 - -`commands.py` 只导入 `agent_sandbox.server`,在主入口调用 `start(port)`;镜像显式安装 server SDK。 -已发布 0.4.0a6 alpha 中的 Provider 原生应用 API 使用 `from agent_sandbox import ProviderSandbox`, -再通过 `await ProviderSandbox.connect(ID)` 连接。这个实例支持 -`async with await sandbox.application_client(9000, token=application_token)`;标准 -`Sandbox`/`AsyncSandbox` 是另一套接口。应用端点解析保留代理前缀,并隔离网关和应用凭据。 -完整连接与关闭示例见[如何访问沙箱](template-server-api.md#如何访问沙箱),最终删除由沙箱创建者负责。 - ---- - -## 发布与部署模板 - -Python SDK 是运行时客户端,不构建源码模板。通常使用 `abx --provider PROVIDER deploy SOURCE --name my-app --yes`,再用 Python 连接返回的沙箱。高级镜像准备使用 `abx template build` 和 `abx deploy`;将准备好的引用传给 `launch_template` 前,确认原生启动路径不会同时启动同一个服务。见[启动方说明](template-workflows.md)和[构建与部署](build-and-deploy.md)。 - -将模板发布到 GitHub、从本地或 registry 安装模板、适配已有项目(`abx template init --adopt`),或通过命令行构建并注册模板(`abx deploy`、`abx template build`),请使用 `abx` CLI;见[相关:abx CLI](#相关abx-cli)。 - -## 下一步 - -- [使用模板](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/zh/guide/using-templates.md) — 安装和使用模板 -- [template.yaml 规范](../reference/template-yaml-spec.md) — 所有字段精确定义 -- [声明式用法](declarative-usage.md) — @sandbox 装饰器 - ---- +在 `sandbox.start` / `sandbox.ready` 声明启动和就绪命令,在 `sandbox.services` 声明端点。YAML 不注册函数,也不会启用服务端默认关闭的组。服务端策略是独立运行时配置,通过镜像内策略文件或服务端文档说明的配置入口加载。参阅[服务端 API](template-server-api.md)。 -## 相关:abx CLI +通过 [CLI 模板编写流程](https://github.com/Alibaba-Agent-Sandbox/agent-sandbox/blob/main/docs/zh/guide/authoring-templates.md)准备和发布源码。Python [启动已准备模板](template-workflows.md),或连接已有 Sandbox。镜像原生入口和 Python 启动命令应只有一个责任方,避免同一服务重复启动。 -模板的命令行工作流记录在 CLI 仓库:[模板编写(CLI)](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/zh/guide/authoring-templates.md)(`abx template init`、`--adopt`、发布、`template init --from`)、[部署与构建](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/zh/guide/deploy-and-build.md)(`abx deploy`、`abx template build`)和[模板使用](https://github.com/alibaba-agent-sandbox/agent-sandbox/blob/main/docs/zh/guide/using-templates.md)。 +通过 `session.servers["app"].commands.run("hello", name="Python")` 或显式解析的 `sandbox.application_client` 调用注册函数。应用令牌、网关请求头和 Sandbox API 凭据属于不同认证层。Shell 使用 `sandbox.commands.run(...)`,其存在不等于业务 API 或解释器能力。 diff --git a/docs/zh/guide/best-practices.md b/docs/zh/guide/best-practices.md index dc15299f..b1e25855 100644 --- a/docs/zh/guide/best-practices.md +++ b/docs/zh/guide/best-practices.md @@ -14,11 +14,13 @@ Dockerfile 的 `FROM`、能否写 `CMD` 或 `ENTRYPOINT`,以及平台的 `star 组只是声明了它的那些端点的名字;端点规则是唯一的开关。在模板策略里用映射关掉默认组: -```yaml -server: - groups: - terminal: false - process: false +```json +{ + "groups": { + "terminal": false, + "process": false + } +} ``` `core` 不能关闭。打开可选组有三种方式:策略里写列表(`groups: [dev_tools]`)、在 `commands.py` 里调用 `default_table().enable_group(CapabilityGroup.DEV_TOOLS)`,或设置环境变量 `ABX_SERVER_ENABLED_GROUPS=dev_tools`。这个变量只能打开组,没有关闭组的变量。`GET /capabilities` 会列出每个端点,以及关闭它的 `rule`。 diff --git a/docs/zh/guide/file-and-command-controls.md b/docs/zh/guide/file-and-command-controls.md index 1f764b90..97ce6320 100644 --- a/docs/zh/guide/file-and-command-controls.md +++ b/docs/zh/guide/file-and-command-controls.md @@ -1,44 +1,81 @@ # 文件与命令控制 -模板如何限制沙箱服务器可以触及的路径和程序。如何打开策略、选择器,以及全部端点 id,见 [细粒度能力](fine-grained-capabilities.md)。这些路由的 HTTP 形状见[模板服务器 HTTP API](template-server-api.md)。`template.yaml` 的 `server:` 块见 [template.yaml 规范](../reference/template-yaml-spec.md#服务端策略server)。设计说明见[细粒度能力](../design/fine-grained-capabilities.md)。 +模板如何限制沙箱服务器可以触及的路径和程序。如何打开策略、选择器,以及全部端点 id,见 [细粒度能力](fine-grained-capabilities.md)。这些路由的 HTTP 形状见[模板服务器 HTTP API](template-server-api.md)。服务端策略独立于 [v1 资源清单](../reference/template-yaml-spec.md#capability-declarations)加载。设计说明见[细粒度能力](../design/fine-grained-capabilities.md)。 实时路由会调用 `check_env`、`check_url` 和 `check_process`,下文的路径检查和命令检查也同样在路由里。`check_env` 用在 `handle_env_set`(`POST /env`)、`enforce_exec_policy`(`POST /shell`、`POST /shell/stream`、`POST /process/start` 的 `env` 对象)和 `_handle_create_session`(`POST /pty/sessions`)。`check_url` 用在 `handle_browser_navigate`(`POST /browser/navigate`)。`check_process` 用在 `enforce_exec_policy`(上述 shell 与进程的 argv)、`handle_code_run`(`POST /code/run`)和 `_handle_create_session`(PTY 的 shell)。拒绝是 HTTP 403,错误码是 guard 自己的码:`env_var_denied`、`url_denied` 或 `process_denied`。空的 env 映射仍然允许。 -## 模板声明什么 - -`template.yaml` 里可选的 `server:`。没有它时用默认策略:端点接收的每个路径都留在 `ABX_SERVER_BASE_DIR`(默认 `/home/user`)之下,包括 `/shell` 和进程启动的 `cwd`,shell 命令不做过滤。工具链把这块编译成 `/etc/agent-sandbox/server-policy.json`。进程在设置了 `ABX_SERVER_POLICY`(内联 JSON)或 `ABX_SERVER_POLICY_FILE` 时也会加载它们。`agent-sandbox-server check-config --policy <文件>` 校验文档并打印 lint 警告。无效文档会阻止启动。 - -```yaml -server: - preset: transfer-only - base_dir: /home/user - groups: - process: false - terminal: false - endpoints: - files_download_stream: - paths: - allow: [/home/user/out] - deny: ["**/.env", "*.pem"] - read_only: false - symlinks: within - limits: - max_bytes: 5MB - files_upload_stream: - paths: - allow: [/home/user/in] - params: - overwrite: false - deny_extensions: [".sh", ".so"] - shell: - params: - allow_commands: ["ls", "cat"] - deny_commands: ["rm"] - limits: - timeout_seconds: 30 - max_output_bytes: 1048576 +这些可选策略用于运行 SandboxServer 的模板。Managed Agents API 桥接服务使用自己的固定路由契约。 + +## 服务端策略文件 + +保存独立的 `server-policy.json`,通过 `ABX_SERVER_POLICY_FILE` 或 `agent-sandbox-server serve --policy server-policy.json` 加载;内联 JSON 使用 `ABX_SERVER_POLICY`。没有策略时,端点接收的路径留在 `ABX_SERVER_BASE_DIR`(默认 `/home/user`)之下,包括 shell/进程的 `cwd`,shell 命令不做过滤。`agent-sandbox-server check-config --policy server-policy.json` 校验文档并打印 lint 警告;无效文档会阻止启动。安装 PyYAML 时也支持 `.yaml`/`.yml` 策略文件;这是独立的运行时策略,与 v1 资源清单分开加载。 + +```json +{ + "preset": "transfer-only", + "base_dir": "/home/user", + "groups": { + "process": false, + "terminal": false + }, + "endpoints": { + "files_download_stream": { + "paths": { + "allow": [ + "/home/user/out" + ], + "deny": [ + "**/.env", + "*.pem" + ], + "read_only": false, + "symlinks": "within" + }, + "limits": { + "max_bytes": "5MB" + } + }, + "files_upload_stream": { + "paths": { + "allow": [ + "/home/user/in" + ] + }, + "params": { + "overwrite": false, + "deny_extensions": [ + ".sh", + ".so" + ] + } + }, + "shell": { + "params": { + "allow_commands": [ + "ls", + "cat" + ], + "deny_commands": [ + "rm" + ] + }, + "limits": { + "timeout_seconds": 30, + "max_output_bytes": 1048576 + } + } + } +} ``` +要将策略放入源码镜像,在 Dockerfile 中显式添加: + +```dockerfile +COPY server-policy.json /etc/agent-sandbox/server-policy.json +``` + +没有显式指定策略来源时,服务端会读取该默认路径。 + 预设在你的端点规则之前生效:`default`(没有额外限制)、`transfer-only`(只留上传和下载端点)、`read-only`(关掉 process、terminal、dev tools、browser,并关掉写端点)、`hardened`(关掉这些组,并在文件路由上加拒绝 glob)、`locked`(只留 `list_commands` 和 `run_command`)。 `paths.allow` 一旦给出,就取代 `base_dir`,成为该端点的边界。`paths.deny` 是目录前缀或 glob;不含 `/` 的模式匹配路径分量(`*.pem`、`.env`)。拒绝优先。`paths.read_only: true` 拒绝该端点上的写和删除。`paths.symlinks` 为 `within`(符号链接的目标留在边界内才允许)或 `never`。 diff --git a/docs/zh/guide/fine-grained-capabilities.md b/docs/zh/guide/fine-grained-capabilities.md index 1eb2cac6..9b57820d 100644 --- a/docs/zh/guide/fine-grained-capabilities.md +++ b/docs/zh/guide/fine-grained-capabilities.md @@ -30,35 +30,48 @@ 三种入口,服务端用同一份文档。无效文档会阻止启动,不会退回成全部允许。 -`template.yaml` 的 `server:` 块。工具链把它编译成 `/etc/agent-sandbox/server-policy.json`。可用的键:`version`、`preset`、`base_dir`、`groups`、`defaults`、`auth`、`endpoints`、`description`。`groups` 可以是要打开的组列表,也可以是组到 `true`/`false` 的映射。 - -```yaml -server: - endpoints: - "*": - enabled: false - agents_sessions_create: - enabled: true - agents_sessions_events_create: - enabled: true - agents_sessions_items_list: - enabled: true - agents_runtimes_list: - enabled: true +编写独立的 `server-policy.json`,将其复制到镜像并在服务端启动时加载;它与 `template.yaml` 分开。JSON 始终可用,安装 PyYAML 时也支持 `.yaml`/`.yml` 策略文件。可用的键:`version`、`preset`、`base_dir`、`groups`、`defaults`、`auth`、`endpoints`、`description`。`groups` 可以是要打开的组列表,也可以是组到 `true`/`false` 的映射。 + +```json +{ + "endpoints": { + "*": { + "enabled": false + }, + "agents_sessions_create": { + "enabled": true + }, + "agents_sessions_events_create": { + "enabled": true + }, + "agents_sessions_items_list": { + "enabled": true + }, + "agents_runtimes_list": { + "enabled": true + } + } +} ``` 组的简写,列表或映射: -```yaml -server: - groups: [agents, dev_tools] # 等价于 {agents: true, dev_tools: true} +```json +{ + "groups": [ + "agents", + "dev_tools" + ] +} ``` -```yaml -server: - groups: - process: false - terminal: false +```json +{ + "groups": { + "process": false, + "terminal": false + } +} ``` 内联 JSON: @@ -72,6 +85,7 @@ export ABX_SERVER_POLICY='{"groups":["agents"],"endpoints":{"*":{"enabled":false ```bash export ABX_SERVER_POLICY_FILE=/etc/agent-sandbox/server-policy.json agent-sandbox-server check-config --policy /etc/agent-sandbox/server-policy.json +agent-sandbox-server serve --policy /etc/agent-sandbox/server-policy.json ``` 查找顺序:`ABX_SERVER_POLICY`,然后 `ABX_SERVER_POLICY_FILE`,然后默认路径上的策略文件。都没有时用内置默认:可选组关,其余全开,没有逐端点限制。 @@ -284,4 +298,4 @@ Agents 端点已经在内置目录里,默认 scope 是 `agents:use`。和 `upl | `agents_sessions_items_list` | `GET /v1/agents/sessions/{id}/items` | | `agents_runtimes_list` | `GET /v1/agents/runtimes` | -本地 e2e 已确认:`POST /shell` 是 404,`POST /v1/agents`(定义)是 404,Codex 和 Qwen Code 的会话、事件、条目仍然 200 / 202。要再开放一条路由,把对应端点 id 加进 `ServerPolicy.only` 和 `template.yaml` 的 `endpoints`。 +本地 e2e 已确认:`POST /shell` 是 404,`POST /v1/agents`(定义)是 404,Codex 和 Qwen Code 的会话、事件、条目仍然 200 / 202。要再开放一条路由,把对应端点 id 加进 `ServerPolicy.only` ,或独立策略文档中的 `endpoints`。 diff --git a/docs/zh/guide/managed-agents-api.md b/docs/zh/guide/managed-agents-api.md index b6dad2d1..4a177f8b 100644 --- a/docs/zh/guide/managed-agents-api.md +++ b/docs/zh/guide/managed-agents-api.md @@ -43,6 +43,8 @@ with httpx.Client( print(response.headers["X-Agent-Sandbox-Submission-Id"]) ``` +Session 的 `created_at` 与 `last_active_at` 为整数秒 Unix 时间戳。`required_actions` 与 `vault_ids` 返回空数组;当前不支持客户端待办动作或 Vault 绑定。 + 创建返回 `201`。接收输入后生成持久 Submission,真正开始执行时才出现公开 Turn。 轮询 Session、`/turns` 和 `/artifacts` 集合,通过 `/v1/agents/sessions/{id}/artifacts/{artifact_id}/content` 下载已发布产物。 @@ -128,6 +130,16 @@ FC 部署要求 PostgreSQL;SQLite 仅用于明确的本地模式。存储、 额度不足返回 `429`。过期结果返回 `410`,已删除资源返回 `404`。 清理会保留解决不确定资源所需的私有归属记录。 +## Sandbox 实例配置 + +v1 `sandbox.instance` 控制每个 Session 新建的 Sandbox:`timeout`、CPU/内存/磁盘/GPU、已有卷名称挂载与用户元数据。托管 TTL 默认3600秒,内部 lifetime 预算由这一值派生。更新应用时,已有 Session 保留其配置快照和 Worker 路由。`sandbox.template.props` 是模板准备参数,不会自动变成实例规格。 + +所有自定义环境值都按敏感数据处理,包括看似普通的值。部署器只在编译配置中保存 `instanceEnvNames`,值仅通过 Worker 的 `ABX_MANAGED_SANDBOX_ENV_JSON` 或 `_FILE` 私有来源解析,不进入 Session settings 或公开 plan。私有映射的名字必须与声明完全一致。模型凭据使用独立模型 binding。`ABX_*`、`OPENAI_*`、`QWEN_*`、`CODEX_*`、`PYTHON*`、`LD_*`、`HOME`、`PATH`、`SHELL`、`USER`、`LOGNAME`、`ANTHROPIC_API_KEY`、`DASHSCOPE_API_KEY` 属于运行时/模型控制,不能由自定义环境覆盖。 + +托管 Sandbox 固定安全访问、超时销毁且不自动恢复;冲突的生命周期或未承接安全字段在创建前拒绝。元数据不能覆盖 `abx-managed`、`abx-session`、`abx-revision`。挂载点不得覆盖独占工作区、私有运行时状态及其祖先或子目录。已有卷属于借用资源,删除 Session 不删除卷本体。当前挂载接口不能表达只读:数据卷可以共享且可写,不同归属用户应使用适当隔离的数据集/卷。Session 工作区独占不代表显式共享的数据卷也是私有的。 + +Worker 自定义环境配置前缀为 `ABX_MANAGED_SANDBOX_ENV`,与其他配置一样只接受 `_FILE` 或 `_JSON` 中的一个来源;Gateway 和 Dispatcher 不读取该来源。 + ## 部署方配置 入口为 `python -m agent_sandbox.managed_agents gateway|worker|dispatcher`。 diff --git a/docs/zh/guide/sdk-usage.md b/docs/zh/guide/sdk-usage.md index e88310f3..ea3100b5 100644 --- a/docs/zh/guide/sdk-usage.md +++ b/docs/zh/guide/sdk-usage.md @@ -261,7 +261,7 @@ async for event in watcher: `sandbox.network` 子模块用于计算端口访问 URL(纯本地计算,不涉及网络请求)。 -> **能力要求**:需要 `ports` 能力。默认能力集不含 `ports`,需在模板的 `template.yaml` 中显式声明。 +> 端口地址由所选 Provider 提供。v1 模板通过 `sandbox.services` 声明服务端点,额外端口使用 `sandbox.ports`;声明本身不会授予 Provider 操作权限。模板启动器要求所选 Provider 对声明端点实际支持 `ports.resolve`。 ```python # 获取端口 URL @@ -414,8 +414,7 @@ policy = { SandboxServer(policy=policy).serve(9000) # a dict or a ServerPolicy ``` -也可以在 `template.yaml` 中声明(`server:` 块,见[模板规范](../reference/template-yaml-spec.md)),或使用 -`agent-sandbox-server serve --policy server-policy.json`。用 `agent-sandbox-server check-config` 校验。 +也可以将策略保存为 `server-policy.json`,运行 `agent-sandbox-server serve --policy server-policy.json`;启动自定义 `commands.py` 时可预先设置 `ABX_SERVER_POLICY_FILE`。使用 `agent-sandbox-server check-config --policy server-policy.json` 校验。详见[策略加载](fine-grained-capabilities.md#如何启动)。 ### 处理拒绝 diff --git a/docs/zh/guide/template-server-api.md b/docs/zh/guide/template-server-api.md index 30e09dc4..cd04067e 100644 --- a/docs/zh/guide/template-server-api.md +++ b/docs/zh/guide/template-server-api.md @@ -97,10 +97,10 @@ PTY WebSocket(端口 9001,路径 `/pty`)检查同样的凭据。它接受 | 位置 | 写法 | |---|---| | `commands.py` | `default_table().enable_group(CapabilityGroup.FILE_OPS)` / `disable_group(...)` | -| `template.yaml` `server:` | `groups: [agents, dev_tools]` 或 `groups: {process: false}`;单个 id 用 `endpoints:` | +| 独立策略文档 | `groups: [agents, dev_tools]` 或 `groups: {process: false}`;单个 id 用 `endpoints:` | | 环境变量 | `ABX_SERVER_ENABLED_GROUPS=agents,dev_tools`(逗号列表,只能打开组) | -没有用来关闭组的环境变量。请在 `server:` 里写 `groups: {process: false}`,或写 `endpoints` 规则。层次、具体程度和 `rule` 的写法见[细粒度能力](fine-grained-capabilities.md)。 +没有用来关闭组的环境变量。请在加载的策略文档中写 `groups: {process: false}`,或写 `endpoints` 规则。层次、具体程度和 `rule` 的写法见[细粒度能力](fine-grained-capabilities.md)。 `provides`、显式工作流、Provider 精确操作和本节端点策略是不同契约。模板不接受旧 `capabilities` 字段;工作流不能自动打开端点。 @@ -621,7 +621,7 @@ screenshot 的 `selector` 匹配不到时是 `404`。策略可以限制 navigate ### `agents`(默认关闭) -用 `ABX_SERVER_ENABLED_GROUPS=agents`、`server:` 里的 `groups: [agents]`,或在 `serve()` 之前调用 `default_table().enable_group(CapabilityGroup.AGENTS)` 打开。70 条路由全部已实现。有本地后端的运行时有两个:Codex CLI(`codex`)和 Qwen Code(`qwen-code`,也接受 `qwen` 和 `qwencode`)。`claude-code`、`cursor` 和 `command` 是已声明、没有本地后端的适配器。`GET /v1/agents/runtimes` 列出每个已声明的运行时,带 `implemented` 和 `installed`。选了未实现的运行时返回 `404` `not_found_error`(`agents.not_implemented_locally`,`x-should-retry: false`),响应体指向 `GET /v1/agents/runtimes`。请求体和 `agents.default`(`ABX_AGENTS_DEFAULT`)都没有指定适配器时,创建调用使用 `codex`。每条 Agents 路由都可以单独启用。端点 id 见[细粒度能力](fine-grained-capabilities.md)。默认 scope 是 `agents:use`。完整路由表见 [Agents API 参考](../reference/agents-api.md)。 +用 `ABX_SERVER_ENABLED_GROUPS=agents`、已加载策略中的 `groups: [agents]`,或在 `serve()` 之前调用 `default_table().enable_group(CapabilityGroup.AGENTS)` 打开。70 条路由全部已实现。有本地后端的运行时有两个:Codex CLI(`codex`)和 Qwen Code(`qwen-code`,也接受 `qwen` 和 `qwencode`)。`claude-code`、`cursor` 和 `command` 是已声明、没有本地后端的适配器。`GET /v1/agents/runtimes` 列出每个已声明的运行时,带 `implemented` 和 `installed`。选了未实现的运行时返回 `404` `not_found_error`(`agents.not_implemented_locally`,`x-should-retry: false`),响应体指向 `GET /v1/agents/runtimes`。请求体和 `agents.default`(`ABX_AGENTS_DEFAULT`)都没有指定适配器时,创建调用使用 `codex`。每条 Agents 路由都可以单独启用。端点 id 见[细粒度能力](fine-grained-capabilities.md)。默认 scope 是 `agents:use`。完整路由表见 [Agents API 参考](../reference/agents-api.md)。 每个响应都带 `X-Agent-Sandbox-Local: 1`,每个对象都带 `x_agent_sandbox` 标记(`local`、`compute`、`differences`)。服务端做不到的字段是带 `param` 的 `400`,绝不会接受后忽略。对象属于调用方;别人的 id 与未知 id 的应答一样。 @@ -748,7 +748,7 @@ curl --fail-with-body -sS "${AUTH[@]}" \ "$ABX_TEMPLATE_URL/commands/hello" ``` -上传和下载。路径必须留在 `ABX_SERVER_BASE_DIR` 之下(默认 `/home/user`)。`file_ops` 必须启用;除非 `server:` 策略或 `commands.py` 把它关掉,否则默认是开的。 +上传和下载。路径必须留在 `ABX_SERVER_BASE_DIR` 之下(默认 `/home/user`)。`file_ops` 必须启用;除非 加载的服务端策略或 `commands.py` 把它关掉,否则默认是开的。 ```bash B64=$(printf 'hello' | base64) diff --git a/docs/zh/guide/template-workflows.md b/docs/zh/guide/template-workflows.md index 30a29b69..21fb04ac 100644 --- a/docs/zh/guide/template-workflows.md +++ b/docs/zh/guide/template-workflows.md @@ -21,7 +21,7 @@ finally: 如果 `X-Access-Token` 请求头未被占用,客户端还会通过服务端已有的这个请求头发送同一个显式应用令牌,使认证能通过 OpenSandbox 1.1.1 等会过滤 `Authorization` 的代理。已有网关请求头(包括注入 HTTP 客户端的默认请求头)不会被覆盖;需要 Bearer 的服务仍可使用原认证方式。 -高级准备场景中,`launch_template` 根据已准备好的 Provider 引用创建新沙箱,并执行清单的 `start_cmd`。该引用的原生启动命令或镜像入口不能同时启动同一个服务。构建产物保留其启动声明,不能把准备好的引用理解为可以重复启动同一服务。如果 CLI 已启动应用,按上文连接它返回的沙箱即可。 +高级准备场景中,`launch_template` 根据已准备好的 Provider 引用创建新沙箱,并执行清单的 `sandbox.start`。该引用的原生启动命令或镜像入口不能同时启动同一个服务。构建产物保留其启动声明,不能把准备好的引用理解为可以重复启动同一服务。如果 CLI 已启动应用,按上文连接它返回的沙箱即可。 确认准备阶段没有重复启动后,让 Python 负责启动和清理: @@ -35,8 +35,8 @@ async with await launch_template( result = await session.run_command("hello", name="Python") ``` -不再选择工作流。Python 创建一个沙箱,执行一次 `start_cmd`,检查 `ready_cmd`, -并解析 `provides` 中所有服务。SandboxServer 协议 v2 服务执行 health/discovery 检查; +不再选择工作流。Python 创建一个沙箱,执行一次 `sandbox.start`,检查 `sandbox.ready`, +并解析 `sandbox.services` 中所有服务。SandboxServer 协议 v2 服务执行 health/discovery 检查; 其他协议由模板的就绪命令检查,返回对应客户端但不猜测其协议路由。 `session.services` 包含全部服务客户端,`session.servers` 包含 SandboxServer 客户端。 只有一个 SandboxServer 时可以使用 `session.server` 和 `session.run_command`; @@ -44,7 +44,7 @@ async with await launch_template( 这不会改变已经整体启动的模板内容。 Python 不构建源码;源码准备由 CLI 负责,按上文选择一个启动方。 -模板也可声明固定摘要的 `image`,或者 `provider` 与 `template_id` 原生模板标识。 +模板也可声明固定摘要的 `sandbox.template.image`,或者 `sandbox.provider` 与 `sandbox.template.reference` 原生模板标识。 引用由 Provider 解释,原生 ID 不需要虚构版本。已有外部 HTTP 服务直接使用 `ServerClient`;应用部署 hook 仍由部署编排器处理。 @@ -53,15 +53,15 @@ Python 不构建源码;源码准备由 CLI 负责,按上文选择一个启 后续服务失败也会关闭之前解析的所有客户端。日志不记录令牌、环境变量值和签名 URL, 也不等于云端验证证据。同一个最终 `ABX_SERVER_TOKEN` 同时用于沙箱环境和客户端。 -收到进程启动确认后,启动器会在一个 `readiness_timeout` 预算内重复执行尚未成功的 `ready_cmd`。每次探测受剩余预算约束,退出码为 0 时才完成就绪检查;预算耗尽会抛出 `readiness-command-failed` 并删除本次创建的沙箱,取消启动同样保留资源清理。进程已经启动与应用已经就绪是两个独立条件。 +收到进程启动确认后,启动器会在一个 `readiness_timeout` 预算内重复执行尚未成功的 `sandbox.ready`。每次探测受剩余预算约束,退出码为 0 时才完成就绪检查;预算耗尽会抛出 `readiness-command-failed` 并删除本次创建的沙箱,取消启动同样保留资源清理。进程已经启动与应用已经就绪是两个独立条件。 -## 从规范 4 迁移 +## 资源配置 -改为 `spec_version: 5`,删除 `workflow_contract_version`、`default_workflow`、 -`workflows`,保留完整的 `provides`、实际 `start_cmd` 和 `ready_cmd`。 -CLI 命令删除 `--workflow`;Python 改用 `launch_template`,不传 `workflow_id`。 -旧清单明确报告迁移错误,不自动挑选以前的默认工作流。 -历史规范 4 证据保留,但不能证明规范 5 的启动链路已验证。 +v1 示例描述更新后的 SDK 源码消费者。已安装发行物尚无该能力时,使用匹配的[源码安装](install-python.md);包版本元信息相同不代表都具备 v1 支持。 + +使用 [v1 资源字段](../reference/template-yaml-spec.md)。解析字面量已准备模板无需安装 CLI,所选 Provider 仍可能有其自身运行时依赖;原生表达式需先由 Devs 解析器解析。`sandbox.instance.timeout` 提供 TTL,只有显式 Python `timeout` 才会覆盖;省略时保留清单值。实例规格、已有卷、环境变量和元数据会实际传入创建请求。环境值不会隐式读取宿主环境。应用部署由协调器负责,Python 在创建 Sandbox 前拒绝这类输入。 + +额外声明的 `sandbox.ports` 通过 `session.endpoints[端口]` 返回 Provider 描述,不为其创建客户端或发送探测。网关请求头只在内存中,不进入公开 plan 或日志。端口声明不保证原生 TCP 可达,也不会启动该服务。 ## 解释器操作 @@ -84,8 +84,8 @@ finally: await sandbox.kill() # 本段代码创建并负责删除这个沙箱 ``` -规范 5 清单声明 `provider: alicloud-express`、`template_id: base` 和 -`provides: {}` 后,可以调用 `launch_template(manifest, provider="alicloud-express")`, +v1 清单声明 `sandbox.provider: alicloud-express`、`sandbox.template.reference: base` 和 +`sandbox.services: {}` 后,可以调用 `launch_template(manifest, provider="alicloud-express")`, 通过异步上下文管理器自动删除自己创建的沙箱。不再声明或选择工作流。 在 `code-interpreter-v1` 上运行 shell 命令,并不代表调用了解释器 API。极速版、E2B 和 REST 的项目适配器尚未接入统一解释器操作,需要时使用已记录的平台原生 SDK;CubeSandbox 和容器版接入了持久默认上下文操作。 diff --git a/docs/zh/guide/troubleshooting.md b/docs/zh/guide/troubleshooting.md index 7b252513..ad42f879 100644 --- a/docs/zh/guide/troubleshooting.md +++ b/docs/zh/guide/troubleshooting.md @@ -124,11 +124,11 @@ export ALICLOUD_ACCESS_KEY_SECRET="your-ak-secret" ### 解决 -1. 确认具体工作流、消费入口和失败条件责任层;使用 [spec4 清单](../reference/template-yaml-spec.md),不能补旧 `capabilities` 或 `tested_on`。 +1. 确认具体工作流、消费入口和失败条件责任层;使用 [v1 资源清单](../reference/template-yaml-spec.md),不能补旧 `capabilities` 或 `tested_on`。 2. 适配器不支持时实现/安装对应操作,或显式选择已支持的工作流。无状态、默认持久上下文、指定上下文和实时流不能互相冒充。 3. 对应用服务,使用 `async with await sandbox.application_client(9000, token=application_token)` 独立解析端点,检查实际监听、协议版本、网关配置及应用令牌。 4. HTTP 401 不做匿名重试;解释器 404 不转 shell。需要 shell 时显式调用命令 API。 -5. 服务端策略另外控制九个端点组;`core` 始终开启,`dev_tools`、`browser`、`agents` 默认关闭。检查 `server:`、实际加载的 policy 和环境配置,而不是模板的旧能力标签。 +5. 服务端策略另外控制九个端点组;`core` 始终开启,`dev_tools`、`browser`、`agents` 默认关闭。检查 `ABX_SERVER_POLICY`、`ABX_SERVER_POLICY_FILE`、实际加载的策略和环境配置,而不是模板的旧能力标签。 见[显式模板工作流](template-workflows.md)和[模板服务器 HTTP API](template-server-api.md)。 diff --git a/docs/zh/reference/api-reference.md b/docs/zh/reference/api-reference.md index fc8274ef..26343167 100644 --- a/docs/zh/reference/api-reference.md +++ b/docs/zh/reference/api-reference.md @@ -367,6 +367,7 @@ from agent_sandbox.api import ServerClient, ServerCapabilities | 成员 | 签名 | 说明 | |------|------|------| | `ServerClient()` | `(base_url, *, token=None, headers=None, pty_url=None, timeout=30.0, http_client=None, expires_at=None)` | `token` 以 `Authorization: Bearer` 发送;`headers` 携带网关的 `X-Access-Token` | +| `ProviderSandbox.application_endpoint()` | `async (port=9000, *, traffic_access_token=None) -> dict[str, Any]` | 解析 Provider 端点和网关请求头,不创建客户端或探测就绪,不保证原生 TCP 可达 | | `ProviderSandbox.application_client()` | `async (port=9000, *, token=None, traffic_access_token=None, timeout=30.0) -> ServerClient` | 解析 Provider 应用网关描述符,保留其路径前缀、查询参数及网关头,应用令牌单独传入;跨 Provider 推荐使用 | | `Sandbox.server` | `cached_property -> ServerClient` | 9000 端口原生本地 URL 辅助入口;跨 Provider 使用 application_client | | `Sandbox.server_at()` | `(port=9000, *, token=None, pty_port=9001, timeout=30.0) -> ServerClient` | 原生本地 URL 辅助入口;需 Provider 网关描述符时使用 application_client;`token` 是应用令牌 | @@ -510,3 +511,5 @@ class SandboxStatus(str, Enum): `ServerClient.commands.invoke(name, arguments, /, *, request_timeout=None)` 和 `ProviderSandbox.invoke(name, arguments, /, *, server_port=9000, application_token=None, request_timeout=None)` 将 arguments 作为命令 JSON 请求体;均有 `invoke_sync`,旧 `run` 保留关键字参数契约。应用服务要求认证时,将其 `ABX_SERVER_TOKEN` 通过 `application_token` 传入。 接受云 AccessKey 的 Provider 生命周期方法同时接受 `security_token`,环境变量为 `ALIBABA_CLOUD_SECURITY_TOKEN`。`ProviderSandbox.create` 另接受 `network`、`allow_internet_access`。`ProviderSandbox.connect(..., traffic_access_token=...)` 保留 GET 只读连接语义;`application_client(..., traffic_access_token=...)` 仅为该应用客户端覆盖网关凭据。详见[提供方命令](../guide/provider-commands.md)和[认证](../guide/authentication.md)。 + +canonical v1 已准备模板使用 `launch_template(..., timeout=None)` 保留清单实例 TTL;显式规格和环境参数覆盖对应值。`TemplateSession.services` 保存实际声明的服务客户端,`TemplateSession.endpoints` 保存附加端口描述,请求头只在内存中。参阅[模板字段](template-yaml-spec.md)与[启动归属及清理](../guide/template-workflows.md)。 diff --git a/docs/zh/reference/configuration.md b/docs/zh/reference/configuration.md index c8fc7e44..84ff71b8 100644 --- a/docs/zh/reference/configuration.md +++ b/docs/zh/reference/configuration.md @@ -117,8 +117,8 @@ | `ABX_SERVER_TOKEN` | — | Server 认证令牌 | | `ABX_SERVER_BASE_DIR` | `/home/user` | 所有接收路径的端点(包括 shell 的 `cwd`)必须留在其下的目录 | | `ABX_SERVER_ENABLED_GROUPS` | — | 要打开的组,逗号分隔,例如 `agents,dev_tools`。唯一的组环境变量,没有用来关闭组的变量 | -| `ABX_SERVER_POLICY` | — | 内联 JSON 策略文档(键与 `server:` 块相同) | -| `ABX_SERVER_POLICY_FILE` | — | 策略 JSON 文件路径 | +| `ABX_SERVER_POLICY` | — | 独立的内联 JSON 策略文档 | +| `ABX_SERVER_POLICY_FILE` | — | 策略 JSON 文件路径;安装 PyYAML 时也支持 `.yaml`/`.yml` | | `ABX_PTY_PORT` | `9001` | PTY 终端 WebSocket 端口 | | `ABX_MAX_UPLOAD_SIZE` | 100 MiB | 解码后上传大小上限 | diff --git a/docs/zh/reference/error-codes.md b/docs/zh/reference/error-codes.md index 042e89ff..9903cd2a 100644 --- a/docs/zh/reference/error-codes.md +++ b/docs/zh/reference/error-codes.md @@ -44,7 +44,7 @@ except SandboxError as e: | E2001 | `TemplateNotFoundError` | 模板不存在 | 运行 `abx template list` 查看可用模板 | 指定了不存在的模板名称 | | E2002 | `QuotaExceededError` | 配额超限 | 销毁空闲沙箱或申请提升配额 | 运行中的沙箱数量超过账户配额 | | E2003 | `RegionUnavailableError` | 区域不可用 | 尝试其他区域或检查服务可用性 | 请求的区域暂时不可用或不存在 | -| E2004 | `TemplateParseError` | 清单解析失败 | 修正 spec4、必需工作流字段及未知/已移除字段 | 当前 YAML 不符合契约,不能以默认权限继续 | +| E2004 | `TemplateParseError` | 清单解析失败 | 修正 specVersion: 1、必需工作流字段及未知/已移除字段 | 当前 YAML 不符合契约,不能以默认权限继续 | | E2009 | `SandboxNotFoundError` | 该 ID 的沙箱不存在 | 运行 `abx list` 查看运行中的沙箱;已销毁或已过期的沙箱无法重新连接 | 对平台返回 HTTP 404 的 ID 调用 `Sandbox.connect()` / `get_info()` / `set_timeout()` / `pause()` / `resume()`(`kill` 仍保持幂等) | ### 排查步骤 diff --git a/docs/zh/reference/template-yaml-spec.md b/docs/zh/reference/template-yaml-spec.md index 338596ea..527503ae 100644 --- a/docs/zh/reference/template-yaml-spec.md +++ b/docs/zh/reference/template-yaml-spec.md @@ -1,119 +1,68 @@ # template.yaml 规范 -当前模板使用 `spec_version: 5`。发布清单的完整权威 schema 是 [awesome-templates/template-v5.schema.json](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v5.schema.json);共享启动契约见 [contracts](https://github.com/Alibaba-Agent-Sandbox/python-sdk/tree/main/contracts)。应用部署拓扑、hooks 和持久状态字段仍由完整 schema 定义。 +使用 `specVersion: 1`,本地资源配置只需一个 `template.yaml`。完整字段类型和各 Provider 的参数子集见 [template v1 schema](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/spec/template-v1.schema.json) 与[模板规范](https://github.com/Alibaba-Agent-Sandbox/awesome-templates/blob/main/docs/template-spec.md)。上架元信息和初始化问题属于可选的 `registry.yaml`;本地运行无需该文件,也无需 `parameters`。 -## 文件位置 +模板文件使用 YAML1.2 Core,必须为严格 UTF-8,最大 1 MiB。`on` 和 `2026-11-12` 等未加引号的日期保持字符串。重复键、别名、不支持的标签、非字符串键及非有限或非 JSON 值会在创建资源前被拒绝。 -每个源码模板的根目录包含唯一的 `template.yaml`。目录索引只定位该文件,不重复声明工作流;托管模板条目内嵌完整声明。Dockerfile、命令模块和依赖文件与清单一起组成源码输入。 +| 字段 | 含义 | +| --- | --- | +| `specVersion` | 整数 `1` | +| `name` | 资源名称 | +| `sandbox.provider` | Provider ID;可由 Python 显式选择的 Provider 补全 | +| `sandbox.template` | `source`、`image`、`reference` 三选一;可带构建 `props` 和 `aliases` | +| `sandbox.instance` | 实例 TTL、环境变量、元数据及 Provider 支持的实例参数 | +| `sandbox.start`、`sandbox.ready` | 整体启动和就绪检查命令 | +| `sandbox.ports` | 可选的附加端口号,不推测协议或自动启动服务 | +| `sandbox.services` | 端点映射:协议、整数版本、端口及可选 operations | +| `deployment` | 原生 Serverless Devs 项目,直接使用组件名和原生 props | +| `application` | 显式选择应用协调器,目前为 `type: agents-api` | -## 完整字段定义 +`sandbox.template.props` 是准备模板时的参数,不是实例覆盖值。引用已有模板时不能声明准备参数。Python 按选定 schema 和 Provider 消费 instance 的 CPU、内存、磁盘、GPU、已有卷名称挂载、环境变量、元数据和 TTL。启动器只接受固定的 `lifecycle: {onTimeout: kill, autoResume: false}`;暂停/自动恢复协调尚未承接,会在创建前拒绝。独立 `ProviderSandbox` 生命周期 API 保持其自身契约。 -### 基础信息 +普通实例 TTL 默认 300 秒,托管 Agents API 默认为 3600 秒。Python 显式 `timeout`、`cpu`、`memory`、`disk`、`gpu`、`volume_mounts` 替换对应实例值;显式 `envs` 和 `metadata` 按名称合并,启动器自己的归属标签保留。内存/磁盘单位与可用性由 Provider 决定;Express 高层 schema 使用整数 MB,不代表所有云账号都能创建这些规格。 -发布 schema 要求 `name`、`version`、`description`、`author`、`license` 等元数据;`tags`、`priority`、`wave`、`smoke` 的规则见完整 schema。SDK 的运行时模型不是整个发布 schema 的替代品。 +原生 `${...}` 表达式必须由真实 Devs 解析器执行。Python 已准备模板启动器接受字面量或已解析的 v1 快照,明确拒绝未解析表达式。环境变量值按字面量使用,空字符串不会隐式读取宿主环境。调用方应自行解析秘密来源,通过 `envs` 或已解析快照传入。Python 不构建源码镜像,也不因某个组件字段存在就承诺云端支持。缺少真实 v1 校验器的自定义 Provider 会被模板启动器拒绝,独立 Provider API 仍可使用。 -### 运行时配置 + +## 服务与服务端策略 -`env` 是字符串映射,`ports` 是端口列表。`start_cmd`、`ready_cmd` 与显式启动方式配合使用;镜像和服务必须实际提供声明的监听端口。SDK 解析这些字段不会自动改写 Dockerfile。 +`sandbox.services` 声明端点,不会启用服务端 API 或授予 Provider 操作。启动器一次启动整个模板,检查 SandboxServer 协议 v2 的健康和发现接口;其他协议使用 `sandbox.ready`,不会推测其路由。 -### 资源规格 +根 `server` 不是 v1 资源字段。SandboxServer 端点策略仍通过其运行时策略文件或 `ABX_SERVER_POLICY_FILE` / `ABX_SERVER_POLICY` 加载。独立的 `validate_server_block` 结构校验器保留,实际服务端负责校验策略值。参阅[细粒度能力](../design/fine-grained-capabilities.md)。命名命令来自 Python `@command` 注册函数,YAML 不生成函数。 -源码发布清单使用 `resources.cpu`、`resources.memory`;SDK 运行时配置还可携带 `cpu_count`、`memory_mb`。应用部署的资源和拓扑字段以完整 schema 为准,不能用工作流规划代替部署执行。 +## 已准备模板示例 -### 沙箱代数 - -`generation` 属于具体模板和提供方启动配置,目录源码模板 schema 当前要求 `2`。是否可以创建,还要检查提供方实际支持的输入和启动方式。 - - -### 模板启动 - -`spec_version` 为整数 `5`。`provides` 通过 `protocol`、`version`、`port`、`operations` -描述全部服务,也可以为空。`start_cmd` 整体启动模板一次;没有它时使用镜像或平台模板 -自身入口。`ready_cmd` 如有声明,在返回服务客户端前执行。固定 `image` 或 -`provider` 与 `template_id` 可以定位已准备模板;其他情况由 CLI 准备源码。 -Python 要求已准备好的引用。 - -从规范 4 迁移时删除 `workflow_contract_version`、`default_workflow`、`workflows`。 -启动时不再选择 application/exec/interpreter 类型或某个操作。旧 `capabilities`、 -`tested_on` 和手填 Provider 兼容表仍拒绝。具体调用能力由 Provider 和服务端实际检查。 -见[模板启动指南](../guide/template-workflows.md)。 - -### 已移除的键:`custom_commands` - -`custom_commands` 被拒绝。命名命令由服务端 `commands.py` 中的 `@command` 注册,客户端通过应用 HTTP 调用,不由 YAML 生成函数。 - -### 服务端策略(`server`) - -可选。声明模板对外暴露哪些 SandboxServer 端点以及各端点的限制(设计见 -[细粒度能力管理](../design/fine-grained-capabilities.md))。省略即保持服务端默认行为。`SandboxTemplate` 会对该块做结构校验(未知键报错); -取值由 `agent-sandbox-server check-config` 校验。服务端读取 `/etc/agent-sandbox/server-policy.json`、`ABX_SERVER_POLICY_FILE` 或 `ABX_SERVER_POLICY`。必须由选定构建/启动流程实际安装策略,仅填写 YAML 不会自动应用。 - -| 键 | 类型 | 说明 | -|----|------|------| -| `preset` | `str` | `default`、`transfer-only`、`read-only`、`hardened`、`locked` | -| `base_dir` | `str` | 相对路径的根目录,也是默认的路径边界 | -| `groups` | `list[str]` 或 `map[str, bool]` | 打开能力组(`[agents, dev_tools]`),或开/关(`{process: false}`)。组是它的端点 id 的别名 | -| `defaults` | 规则 | 先于各端点自身规则应用于所有端点 | -| `endpoints` | `map[str, 规则]` | 以端点 id 为键的逐端点规则(`files_download_stream`、`shell` 等) | -| `auth` | map | `tokens`(`id`、`scopes`、`token_env` 或 `token_sha256`)、`allow_anonymous`、`anonymous_scopes` | - -规则包含 `enabled`、`scopes`、`paths`(`allow`、`deny`、`read_only`、`symlinks`)、`limits`(`max_bytes`、`max_request_bytes`、`timeout_seconds`、`rate_per_minute` 等)、`params`(`overwrite`、`deny_extensions`、`allow_commands` 等)和 `audit`。 +引用的模板必须已经包含应用及服务端依赖,已有卷必须对该 Provider/账号可用。Python 只创建并拥有 Sandbox,不删除借用卷本体。 ```yaml -server: - preset: transfer-only - endpoints: - files_download_stream: - paths: { allow: [/home/user/out] } - limits: { max_bytes: 5MB } - files_upload_stream: - paths: { allow: [/home/user/in] } - params: { overwrite: false } +specVersion: 1 +name: python-hello +sandbox: + provider: alicloud-express + template: + reference: base + instance: + timeout: 600 + cpu: 2 + memory: 2048 + envs: + APP_MESSAGE: hello + metadata: + purpose: example + volume_mounts: + /data: existing-volume-name + start: python /home/user/app/commands.py + ready: agent-sandbox-server healthcheck --port 9000 + services: + app: + protocol: agent-sandbox-server + version: 2 + port: 9000 + operations: [commands.list, commands.run] ``` +源码准备时将 `reference` 换为 `source: {dockerfile: Dockerfile, context: .}` 并使用 CLI,Python 仍需准备后的引用。启动命令和镜像原生入口应只有一个启动责任方,避免同一服务启动两次。参阅[启动与清理](../guide/template-workflows.md)。 -## 完整示例 - -此源码发布示例包含 schema 要求的元数据;基础镜像占位符必须通过显式配置落实后才能构建。 - -```yaml -name: my-python-template -version: 1.0.1 -description: Scaffold for a new Agent Sandbox template. Not a catalog entry. -author: alibaba-agent-sandbox -license: Apache-2.0 -spec_version: 5 -base: fc-e2b-registry..cr.aliyuncs.com/runtime/base: -generation: 2 -start_cmd: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py -ready_cmd: agent-sandbox-server healthcheck --port 9000 -resources: - cpu: 2 - memory: 2048 -ports: -- 9000 -tags: -- example -- skeleton -priority: P2 -wave: other -env: - ABX_SERVER_TOKEN: '' -smoke: -- command: hello - args: - name: Express - expect: Hello, Express! -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - commands.list - - commands.run -``` - -## 构建说明 +Provider 作者可覆盖可选的同步 `Provider.validate_template_input(stage, value)`,使用该版本的真实 schema。阶段为 `template.prepare`(模板对象)和 `sandbox.create`(实例对象),返回 `False` 或抛错表示拒绝。继承的默认方法不算校验器,异步校验会被拒绝。校验通过不授予操作能力,也不代表高层创建 API 自动承接了尚未实现的实例字段。 -`base` 是合法的来源/模板元数据;构建以选定的有效 Dockerfile 和显式构建配置为准,不自动替换 `FROM`、用户、复制路径或入口。依赖通过镜像中的原生依赖文件安装。Python 启动器不准备源码,使用已准备的 `image`/`managed` 引用或显式 `service` 路径。 +`sandbox.ports` 保留浏览器调试、数据库等可选附加端口;编译端口列表为它与服务端口的排序去重并集。`session.endpoints[端口]` 返回未归属 `sandbox.services` 的附加端口的 Provider 描述,网关请求头只在内存中。它不创建 ServerClient,也不发送 health/discovery 请求。声明端口不保证原生 TCP 或公网可达,实际地址与协议由 Provider 决定。也可单独调用 `await sandbox.application_endpoint(port)`,已有服务客户端仍在 `session.services`。 diff --git a/examples/templates/agents-cli/server-policy.json b/examples/templates/agents-cli/server-policy.json new file mode 100644 index 00000000..a3109ceb --- /dev/null +++ b/examples/templates/agents-cli/server-policy.json @@ -0,0 +1,25 @@ +{ + "endpoints": { + "*": { + "enabled": false + }, + "agents_sessions_create": { + "enabled": true + }, + "agents_sessions_get": { + "enabled": true + }, + "agents_sessions_list": { + "enabled": true + }, + "agents_sessions_events_create": { + "enabled": true + }, + "agents_sessions_items_list": { + "enabled": true + }, + "agents_runtimes_list": { + "enabled": true + } + } +} diff --git a/examples/templates/agents-cli/template.yaml b/examples/templates/agents-cli/template.yaml index 16bfd74f..ca7130b6 100644 --- a/examples/templates/agents-cli/template.yaml +++ b/examples/templates/agents-cli/template.yaml @@ -1,42 +1,19 @@ # FIXTURE — offline only; publishable templates live in -# https://github.com/alibaba-agent-sandbox/awesome-templates +# https://github.com/Alibaba-Agent-Sandbox/awesome-templates +specVersion: 1 name: agents-cli -version: 1.0.0 -description: Agents API for Codex and Qwen Code. The image does not preinstall either - CLI. -author: alibaba-agent-sandbox -tags: -- agents-api -- codex -- qwen-code -- self-hosted -base: python:3.12-slim -spec_version: 5 -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - agents.sessions.create -ports: -- 9000 -server: - endpoints: - '*': - enabled: false - agents_sessions_create: - enabled: true - agents_sessions_get: - enabled: true - agents_sessions_list: - enabled: true - agents_sessions_events_create: - enabled: true - agents_sessions_items_list: - enabled: true - agents_runtimes_list: - enabled: true -resources: - cpu: 1 - memory: 1024 +sandbox: + provider: alicloud-express + template: + source: + dockerfile: Dockerfile + props: + cpu: 1 + memory: 1024 + services: + app: + protocol: agent-sandbox-server + version: 2 + port: 9000 + operations: + - agents.sessions.create diff --git a/examples/templates/python-hello/template.yaml b/examples/templates/python-hello/template.yaml index 6f9c00f2..67263195 100644 --- a/examples/templates/python-hello/template.yaml +++ b/examples/templates/python-hello/template.yaml @@ -1,24 +1,21 @@ # FIXTURE — offline only; publishable templates live in -# https://github.com/alibaba-agent-sandbox/awesome-templates +# https://github.com/Alibaba-Agent-Sandbox/awesome-templates +specVersion: 1 name: python-hello -version: 1.0.0 -description: A minimal Python hello world template for testing -author: alibaba-agent-sandbox -tags: -- python -- hello-world -- example -spec_version: 5 -provides: - app: - protocol: agent-sandbox-server - version: 2 - port: 9000 - operations: - - commands.list - - commands.run -start_cmd: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py -ports: -- 9000 -env: - LANG: C.UTF-8 +sandbox: + provider: alicloud-express + template: + source: + dockerfile: Dockerfile + services: + app: + protocol: agent-sandbox-server + version: 2 + port: 9000 + operations: + - commands.list + - commands.run + instance: + envs: + LANG: C.UTF-8 + start: agent-sandbox-server serve --port 9000 --commands /home/user/app/commands.py diff --git a/scripts/check_dists.py b/scripts/check_dists.py index 8177a773..d6c2bd21 100644 --- a/scripts/check_dists.py +++ b/scripts/check_dists.py @@ -50,7 +50,12 @@ assert all(callable(cls.create) for cls in classes) assert callable(launch_template) and callable(sandbox) assert not hasattr(sandbox, "run") and not hasattr(sandbox, "run_async") -assert yaml.safe_load("spec_version: 5") == {"spec_version": 5} +assert yaml.safe_load("specVersion: 1") == {"specVersion": 1} +from agent_sandbox.template_contract import normalize_document, compile_literal_document +literal = {"specVersion": 1, "name": "packaging", "sandbox": { + "provider": "alicloud-express", "template": {"reference": "base"}}} +assert normalize_document(literal)["sandbox"]["instance"]["timeout"] == 300 +assert compile_literal_document(literal, provider="alicloud-express")["template_id"] == "base" assert importlib.util.find_spec("agent_sandbox.server") is None try: importlib.metadata.distribution("agent-sandbox-server") diff --git a/scripts/sync_template_v1_contract.py b/scripts/sync_template_v1_contract.py new file mode 100644 index 00000000..9ec6da75 --- /dev/null +++ b/scripts/sync_template_v1_contract.py @@ -0,0 +1,39 @@ +"""Sync exact template-owner Git object bytes; never regenerate the contract locally.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source_checkout", type=Path) + parser.add_argument("--revision", help="Full immutable producer commit; defaults to source lock") + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + destination = Path(__file__).resolve().parents[1] / "client-sdk/src/agent_sandbox/template_contract" + lock_path = destination / "source.json" + lock = json.loads(lock_path.read_text()) + revision = args.revision or lock["revision"] + if len(revision) != 40 or any(c not in "0123456789abcdef" for c in revision): + parser.error("Use a full immutable producer Git commit") + for source, entry in lock["files"].items(): + data = subprocess.check_output(["git", "show", f"{revision}:{source}"], cwd=args.source_checkout) + target = destination / entry["target"] + digest = hashlib.sha256(data).hexdigest() + if args.check: + if revision != lock["revision"] or target.read_bytes() != data or entry["sha256"] != digest: + raise SystemExit("Template owner source lock or copied bytes differ") + else: + target.write_bytes(data) + entry["sha256"] = digest + if not args.check: + lock["revision"] = revision + lock_path.write_text(json.dumps(lock, indent=2) + "\n") + + +if __name__ == "__main__": + main()