[WIP] Publish Go and TypeScript contracts from one independently owned source #33

Closed
weichen wants to merge 2 commits from feature/versioned-dependencies into main
13 changed files with 1544 additions and 5 deletions
+707
View File
@@ -0,0 +1,707 @@
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
env:
GOPROXY: https://goproxy.cn,direct
GOTOOLCHAIN: local
GOWORK: "off"
GOPRIVATE: git-ssh.u1680832.nyat.app/weichen
steps:
- name: Checkout
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: &trusted_acquisition_1 |
set -euo pipefail
python3 - <<'PY_TRUSTED'
# Trusted workflow code only: never import or execute checkout scripts here.
# Token scopes, workflow provenance and runner isolation are platform NOT_RUN.
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
import tempfile
from urllib.parse import urlsplit
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed = urlsplit(SERVER)
if (parsed.scheme not in {"http", "https"} or not parsed.netloc or
parsed.username or parsed.password or parsed.query or parsed.fragment):
sys.exit("Acquisition requires the platform-injected http(s) server")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/"
REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native",
"Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend",
"Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM",
"Development-Guidelines", "Agent-System-Documents"}
LEGACY = {
"modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core",
"deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System", "pins/agent-system-backend": "Agent-System-Backend",
"pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend",
"pins/agent-system-ui": "Agent-System-UI",
}
GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"}
GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS}
GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in
("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
# Go environment files and inherited Git tracing/config must not change acquisition.
clean = {k: v for k, v in os.environ.items()
if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and
not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))}
clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null",
GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly",
GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/"))
if os.environ.get("npm_config_cache"):
clean["npm_config_cache"] = os.environ["npm_config_cache"]
config = {"core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "",
"credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never", "protocol." + parsed.scheme + ".allow": "always"}
root = Path.cwd()
mirrors = root / ".ci-mirrors"
def process_env(values):
env = clean | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return env
def git(directory, *args, token="", repo=""):
values = config.copy()
if token:
if repo not in REPOS:
raise ValueError("Credential destination is outside the reviewed repository allowlist")
values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token
result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values),
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.stdout
def fixed_sha(value):
if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value):
raise ValueError("Source acquisition requires a fixed commit SHA")
return value.lower()
def mirror(repo, sha, token):
sha = fixed_sha(sha)
destination = mirrors / (repo + ".git")
if destination.is_symlink() or mirrors.is_symlink():
raise ValueError("Mirror directory cannot be a symlink")
mirrors.mkdir(exist_ok=True)
if not destination.exists():
destination.mkdir()
git(destination, "init", "--bare", "-q")
url = f"{SERVER}/weichen/{repo}.git"
try:
git(destination, "cat-file", "-e", sha + "^{commit}")
except subprocess.CalledProcessError:
git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo)
git(destination, "update-ref", "refs/heads/main", sha)
git(destination, "symbolic-ref", "HEAD", "refs/heads/main")
git(destination, "update-ref", "refs/ci/" + sha, sha)
return destination
def mirror_env():
values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""}
# Distinct keys use repeated .insteadOf values, represented as a sequence below.
pairs = list(values.items())
for repo in sorted(REPOS):
destination = mirrors / (repo + ".git")
if not destination.exists():
continue
key = "url." + destination.as_uri() + ".insteadOf"
pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git",
SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo))
env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))}
for index, (key, value) in enumerate(pairs):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"}
(root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items()))
return env
def materialize_legacy(directory, depth=0):
if depth > 16:
raise ValueError("Historical submodule depth exceeds the checkout limit")
links = {}
for entry in git(directory, "ls-tree", "-rz", "HEAD").split(b"\0"):
if entry:
metadata, path = entry.split(b"\t", 1)
mode, kind, commit = metadata.decode().split()
if mode == "160000":
links[path.decode()] = commit
if not links:
return
raw = git(directory, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
entries = {}
for item in raw.split(b"\0"):
if not item:
continue
key, value = item.decode().split("\n", 1)
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
if not match:
raise ValueError("Unsupported candidate .gitmodules setting")
name, field = match.groups()
if field in entries.setdefault(name, {}):
raise ValueError("Duplicate candidate .gitmodules setting")
entries[name][field] = value
if set(entries) != set(links):
raise ValueError("Submodule names must match fixed gitlink paths")
for name, fields in entries.items():
repo = LEGACY.get(name)
if not repo or fields.get("path") != name or fields.get("url") not in {
SSH + repo + ".git", f"{SERVER}/weichen/{repo}.git"}:
raise ValueError("Historical submodule source is outside the reviewed allowlist")
for name in entries:
repo = LEGACY[name]
sha = links[name]
url = f"{SERVER}/weichen/{repo}.git"
git(directory, "config", f"submodule.{name}.url", url)
# Trusted explicit checkout prevents candidate update commands from running.
git(directory, "submodule", "update", "--init", "--checkout", "--", name, token=component_token, repo=repo)
child = directory / name
if git(child, "rev-parse", "HEAD").decode().strip() != sha:
raise ValueError("Historical submodule does not match its fixed SHA")
mirror(repo, sha, component_token)
materialize_legacy(child, depth + 1)
def materialize_manifest(directory):
manifest = directory / "components.json"
lock = directory / "components.lock.json"
if not manifest.exists() and not lock.exists():
materialize_legacy(directory)
return
data = json.loads(manifest.read_text())
locked = json.loads(lock.read_text())
if data.get("schema") != 1 or locked.get("schema") != 1:
raise ValueError("Unsupported source manifest schema")
entries = {}
for item in data["components"]:
repo = item["id"]
path = ".workspace/" + repo
if (repo not in REPOS or repo in entries or item["path"] != path or
item["repo"] not in {SSH + repo + ".git", f"{SERVER}/weichen/{repo}.git"} or
item.get("role") not in {"go-library", "npm-library", "application", "guidelines", "documentation"}):
raise ValueError("Candidate source manifest is outside the reviewed allowlist")
for module in item.get("modules", []):
if module.get("path") not in GO_MODULES or not module["path"].startswith(GO_PREFIX + repo + ".git"):
raise ValueError("Candidate module mapping is outside the reviewed allowlist")
entries[repo] = item
selected = locked["components"]
if set(selected) - set(entries):
raise ValueError("Source lock refers to an unregistered repository")
for repo, item in entries.items():
if item["role"] in {"application", "guidelines", "documentation"} and repo not in selected:
raise ValueError("Application and documentation source needs a fixed lock")
for repo, pin in selected.items():
fixed_sha(pin["commit"])
destination = directory / entries[repo]["path"]
if destination.exists() or destination.is_symlink() or destination.parent.is_symlink():
raise ValueError("Source materialization requires a fresh destination")
# Validate the complete lock before any authenticated component fetch.
for repo, pin in selected.items():
sha = fixed_sha(pin["commit"])
destination = directory / entries[repo]["path"]
destination.mkdir(parents=True)
url = f"{SERVER}/weichen/{repo}.git"
git(destination, "init", "-q")
git(destination, "remote", "add", "origin", url)
git(destination, "fetch", "--depth=1", "origin", sha, token=component_token, repo=repo)
git(destination, "checkout", "-q", "--detach", "FETCH_HEAD")
if git(destination, "rev-parse", "HEAD").decode().strip() != sha:
raise ValueError("Materialized source differs from the fixed lock")
# Persist the registered identity; process config chooses the transport.
git(destination, "remote", "set-url", "origin", entries[repo]["repo"])
mirror(repo, sha, component_token)
materialize_legacy(destination)
try:
if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Core":
raise ValueError("Checkout repository is outside the reviewed allowlist")
sha = fixed_sha(os.environ.get("GITHUB_SHA", ""))
if not root_token:
raise ValueError("Checkout requires TOKEN")
if (root / ".git").exists():
raise ValueError("Checkout requires a fresh workspace")
git(root, "init", "-q")
git(root, "remote", "add", "origin", SERVER + "/weichen/Agent-System-Core.git")
git(root, "fetch", "--depth=1", "origin", sha, token=root_token, repo="Agent-System-Core")
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
if git(root, "rev-parse", "HEAD").decode().strip() != sha:
raise ValueError("Checked-out commit differs from the platform commit")
if (root / ".ci-mirrors").exists() or (root / ".ci-mirrors.env").exists():
raise ValueError("Candidate cannot provide trusted prefetch state")
materialize_manifest(root)
mirror_env()
except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error:
# Do not print child stderr or environment: a failed credential fetch stays redacted.
sys.exit("Checkout refused: " + str(error))
print("Fixed source checkout complete; credentials were not persisted")
PY_TRUSTED
unset TOKEN SUBMODULE_TOKEN
- name: Install Go 1.27.1
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Prefetch Go modules without executing candidate code
env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: &trusted_acquisition_2 |
set -euo pipefail
python3 - <<'PY_TRUSTED'
# Trusted workflow code only: never import or execute checkout scripts here.
# Token scopes, workflow provenance and runner isolation are platform NOT_RUN.
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
import tempfile
from urllib.parse import urlsplit
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed = urlsplit(SERVER)
if (parsed.scheme not in {"http", "https"} or not parsed.netloc or
parsed.username or parsed.password or parsed.query or parsed.fragment):
sys.exit("Acquisition requires the platform-injected http(s) server")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/"
REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native",
"Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend",
"Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM",
"Development-Guidelines", "Agent-System-Documents"}
LEGACY = {
"modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core",
"deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System", "pins/agent-system-backend": "Agent-System-Backend",
"pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend",
"pins/agent-system-ui": "Agent-System-UI",
}
GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"}
GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS}
GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in
("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
# Go environment files and inherited Git tracing/config must not change acquisition.
clean = {k: v for k, v in os.environ.items()
if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and
not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))}
clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null",
GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly",
GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/"))
if os.environ.get("npm_config_cache"):
clean["npm_config_cache"] = os.environ["npm_config_cache"]
config = {"core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "",
"credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never", "protocol." + parsed.scheme + ".allow": "always"}
root = Path.cwd()
mirrors = root / ".ci-mirrors"
def process_env(values):
env = clean | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return env
def git(directory, *args, token="", repo=""):
values = config.copy()
if token:
if repo not in REPOS:
raise ValueError("Credential destination is outside the reviewed repository allowlist")
values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token
result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values),
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.stdout
def fixed_sha(value):
if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value):
raise ValueError("Source acquisition requires a fixed commit SHA")
return value.lower()
def mirror(repo, sha, token):
sha = fixed_sha(sha)
destination = mirrors / (repo + ".git")
if destination.is_symlink() or mirrors.is_symlink():
raise ValueError("Mirror directory cannot be a symlink")
mirrors.mkdir(exist_ok=True)
if not destination.exists():
destination.mkdir()
git(destination, "init", "--bare", "-q")
url = f"{SERVER}/weichen/{repo}.git"
try:
git(destination, "cat-file", "-e", sha + "^{commit}")
except subprocess.CalledProcessError:
git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo)
git(destination, "update-ref", "refs/heads/main", sha)
git(destination, "symbolic-ref", "HEAD", "refs/heads/main")
git(destination, "update-ref", "refs/ci/" + sha, sha)
return destination
def mirror_env():
values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""}
# Distinct keys use repeated .insteadOf values, represented as a sequence below.
pairs = list(values.items())
for repo in sorted(REPOS):
destination = mirrors / (repo + ".git")
if not destination.exists():
continue
key = "url." + destination.as_uri() + ".insteadOf"
pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git",
SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo))
env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))}
for index, (key, value) in enumerate(pairs):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"}
(root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items()))
return env
def source_roots():
bases = [root]
composition = root / ".ci-composition-dev"
if composition.is_dir() and not composition.is_symlink():
bases.append(composition)
paths = []
for base in bases:
paths += [base, base / "rdm"] + [base / path for path in LEGACY] + [base / ".workspace" / repo for repo in sorted(REPOS)]
return [path for path in paths if path.is_dir() and not path.is_symlink()]
try:
targets = []
for source in source_roots():
for suffix in (".", "examples/trace", "adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad"):
directory = source / suffix
if (directory / "go.mod").is_file() and directory.resolve() not in targets:
targets.append(directory.resolve())
# Parse using the installed local Go tool without credentials or candidate execution.
original_files = {}
for directory in targets:
parsed_mod = json.loads(subprocess.check_output(["go", "mod", "edit", "-json"], cwd=directory, env=clean))
canonical = parsed_mod["Module"]["Path"].startswith(GO_PREFIX)
for name in ("go.mod", "go.sum"):
dependency_file = directory / name
if dependency_file.is_file():
original_files[(directory, name)] = dependency_file.read_bytes()
elif canonical and name == "go.sum":
original_files[(directory, name)] = None
for dep in (parsed_mod.get("Require") or []):
path = dep["Path"]
if path.startswith(GO_PREFIX) and path not in GO_MODULES:
raise ValueError("Go dependency is outside the reviewed module mapping")
for replacement in (parsed_mod.get("Replace") or []):
path = replacement["New"]["Path"]
if canonical and not replacement["New"].get("Version"):
raise ValueError("Canonical module cannot acquire a local path replacement")
if path.startswith(GO_PREFIX) and path not in GO_MODULES:
raise ValueError("Go replacement is outside the reviewed module mapping")
auth = config.copy()
for repo in sorted(GO_REPOS):
url = f"{SERVER}/weichen/{repo}.git"
auth[f"url.{url}.insteadOf"] = "https://" + GO_PREFIX + repo
auth[f"http.{url}.extraheader"] = "AUTHORIZATION: token " + component_token
def objects(output):
decoder = json.JSONDecoder()
remaining = output.strip()
while remaining:
item, end = decoder.raw_decode(remaining)
remaining = remaining[end:].lstrip()
yield item
def unchanged():
for (owner, name), contents in original_files.items():
dependency_file = owner / name
current = dependency_file.read_bytes() if dependency_file.exists() else None
if current != contents:
raise ValueError("Go acquisition modified a committed Go dependency file: " + str(dependency_file))
for directory in targets:
# Resolve private go.mod metadata only through exact repository headers;
# validate the complete selected graph before acquiring module zip files.
graph = subprocess.run(["go", "list", "-mod=readonly", "-m", "-json", "all"], cwd=directory,
env=process_env(auth), capture_output=True, text=True)
if graph.returncode:
raise ValueError("Go selected graph resolution failed: " + graph.stderr.strip())
for item in objects(graph.stdout):
if item.get("Main"):
continue
selected = item.get("Replace") or item
for path in (item.get("Path", ""), selected.get("Path", "")):
if path.startswith(GO_PREFIX) and path not in GO_MODULES:
raise ValueError("Selected Go module is outside the reviewed mapping")
unchanged()
# Go 1.27 can prune go.sum even under -mod=readonly for this
# metadata-only command. Use a disposable copy, then validate actual
# package loading against the unchanged committed source locks.
local_replacements = json.loads(subprocess.check_output(
["go", "mod", "edit", "-json"], cwd=directory, env=clean)).get("Replace") or []
downloaded = ""
if not any(not item["New"].get("Version") for item in local_replacements):
with tempfile.TemporaryDirectory(prefix="ci-go-prefetch-") as temporary:
download_root = Path(temporary)
for name in ("go.mod", "go.sum"):
if (directory / name).is_file():
(download_root / name).write_bytes((directory / name).read_bytes())
result = subprocess.run(["go", "mod", "download", "-json"], cwd=download_root,
env=process_env(auth), capture_output=True, text=True)
if result.returncode:
raise ValueError("Go module acquisition failed: " + result.stderr.strip())
downloaded = result.stdout
unchanged()
for item in objects(downloaded):
path = item.get("Path", "")
if path.startswith(GO_PREFIX):
if path not in GO_MODULES:
raise ValueError("Resolved Go module is outside the reviewed mapping")
repo = path[len(GO_PREFIX):].split(".git", 1)[0]
origin = item.get("Origin") or json.loads(Path(item["Info"]).read_text()).get("Origin") or {}
mirror(repo, fixed_sha(origin.get("Hash")), component_token)
# Package metadata loads actual test/build dependencies without running
# vet, compilers, package init functions or candidate tests. No CGO tools.
packages = subprocess.run(["go", "list", "-mod=readonly", "-deps", "-test", "./..."], cwd=directory,
env=process_env(auth) | {"CGO_ENABLED": "0"}, capture_output=True, text=True)
if packages.returncode:
raise ValueError("Go test/build dependency acquisition failed: " + packages.stderr.strip())
unchanged()
mirror_env()
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit("Go prefetch refused: " + str(error))
print("Go modules cached with GOTOOLCHAIN=local; no candidate code executed")
PY_TRUSTED
unset TOKEN SUBMODULE_TOKEN
- name: Vet, test, and race
run: |
set -euo pipefail
source .ci-mirrors.env
GOWORK=off go vet -mod=readonly ./... && GOWORK=off go test -mod=readonly -count=1 ./... && GOWORK=off go test -mod=readonly -count=1 -race ./...
- name: Install Node 22
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Prefetch npm packages without lifecycle scripts
env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: &trusted_acquisition_3 |
set -euo pipefail
python3 - <<'PY_TRUSTED'
# Trusted workflow code only: never import or execute checkout scripts here.
# Token scopes, workflow provenance and runner isolation are platform NOT_RUN.
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
import tempfile
from urllib.parse import urlsplit
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed = urlsplit(SERVER)
if (parsed.scheme not in {"http", "https"} or not parsed.netloc or
parsed.username or parsed.password or parsed.query or parsed.fragment):
sys.exit("Acquisition requires the platform-injected http(s) server")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/"
REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native",
"Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend",
"Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM",
"Development-Guidelines", "Agent-System-Documents"}
LEGACY = {
"modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core",
"deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System", "pins/agent-system-backend": "Agent-System-Backend",
"pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend",
"pins/agent-system-ui": "Agent-System-UI",
}
GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"}
GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS}
GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in
("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
# Go environment files and inherited Git tracing/config must not change acquisition.
clean = {k: v for k, v in os.environ.items()
if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and
not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))}
clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null",
GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly",
GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/"))
if os.environ.get("npm_config_cache"):
clean["npm_config_cache"] = os.environ["npm_config_cache"]
config = {"core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "",
"credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never", "protocol." + parsed.scheme + ".allow": "always"}
root = Path.cwd()
mirrors = root / ".ci-mirrors"
def process_env(values):
env = clean | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return env
def git(directory, *args, token="", repo=""):
values = config.copy()
if token:
if repo not in REPOS:
raise ValueError("Credential destination is outside the reviewed repository allowlist")
values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token
result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values),
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.stdout
def fixed_sha(value):
if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value):
raise ValueError("Source acquisition requires a fixed commit SHA")
return value.lower()
def mirror(repo, sha, token):
sha = fixed_sha(sha)
destination = mirrors / (repo + ".git")
if destination.is_symlink() or mirrors.is_symlink():
raise ValueError("Mirror directory cannot be a symlink")
mirrors.mkdir(exist_ok=True)
if not destination.exists():
destination.mkdir()
git(destination, "init", "--bare", "-q")
url = f"{SERVER}/weichen/{repo}.git"
try:
git(destination, "cat-file", "-e", sha + "^{commit}")
except subprocess.CalledProcessError:
git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo)
git(destination, "update-ref", "refs/heads/main", sha)
git(destination, "symbolic-ref", "HEAD", "refs/heads/main")
git(destination, "update-ref", "refs/ci/" + sha, sha)
return destination
def mirror_env():
values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""}
# Distinct keys use repeated .insteadOf values, represented as a sequence below.
pairs = list(values.items())
for repo in sorted(REPOS):
destination = mirrors / (repo + ".git")
if not destination.exists():
continue
key = "url." + destination.as_uri() + ".insteadOf"
pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git",
SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo))
env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))}
for index, (key, value) in enumerate(pairs):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"}
(root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items()))
return env
def source_roots():
bases = [root]
composition = root / ".ci-composition-dev"
if composition.is_dir() and not composition.is_symlink():
bases.append(composition)
paths = []
for base in bases:
paths += [base, base / "rdm"] + [base / path for path in LEGACY] + [base / ".workspace" / repo for repo in sorted(REPOS)]
return [path for path in paths if path.is_dir() and not path.is_symlink()]
def git_package(value):
if not isinstance(value, str):
return None
if value.startswith(("file:", "link:", "workspace:")):
raise ValueError("Formal npm acquisition cannot consume a local package override")
if value.startswith(("http://", "https://")):
registry = urlsplit(value)
if registry.scheme != "https" or registry.hostname != "registry.npmjs.org" or registry.username or registry.password:
raise ValueError("npm tarball source is outside the reviewed public registry")
elif "/" in value and not value.startswith(("git+", "npm:")):
raise ValueError("npm repository shorthand is outside the fixed-SHA package mapping")
if value.startswith("git+") or value.startswith(("git://", "ssh://", "github:", "gitlab:", "bitbucket:")):
for repo in ("Agent-System-Core", "Agent-System-UI"):
prefix = "git+" + SSH + repo + ".git#"
if value.startswith(prefix):
return repo, fixed_sha(value[len(prefix):])
raise ValueError("npm Git source is outside the reviewed fixed-SHA package mapping")
return None
try:
targets = []
selected = set()
for directory in source_roots():
if not (directory / "package.json").is_file():
continue
if (directory / ".npmrc").exists():
raise ValueError("Candidate .npmrc is not allowed during private dependency acquisition")
package = json.loads((directory / "package.json").read_text())
lock = json.loads((directory / "package-lock.json").read_text())
if lock.get("lockfileVersion") not in {2, 3}:
raise ValueError("npm acquisition requires a package lock")
for group in ("dependencies", "devDependencies", "optionalDependencies"):
for value in package.get(group, {}).values():
found = git_package(value)
if found:
selected.add(found)
for entry in lock.get("packages", {}).values():
found = git_package(entry.get("resolved"))
if found:
selected.add(found)
for group in ("dependencies", "devDependencies", "optionalDependencies"):
for value in entry.get(group, {}).values():
found = git_package(value)
if found:
selected.add(found)
targets.append(directory)
# Fetch only registered fixed Git package commits. npm never receives token config.
for repo, sha in sorted(selected):
repository = mirror(repo, sha, component_token)
package = json.loads(git(repository, "show", sha + ":package.json"))
forbidden = {"prepare", "prepack", "prepublish", "prepublishOnly", "preinstall", "install", "postinstall"}
if forbidden & set(package.get("scripts", {})):
raise ValueError("Git package provider must commit outputs without acquisition lifecycle scripts")
if not git(repository, "ls-tree", sha, "package/").strip():
raise ValueError("Git package provider is missing committed package outputs")
no_auth = mirror_env()
empty_config = tempfile.NamedTemporaryFile(prefix="ci-npm-globalrc-")
for directory in targets:
result = subprocess.run(["npm", "ci", "--ignore-scripts", "--no-audit", "--no-fund",
"--userconfig=/dev/null", "--globalconfig=" + empty_config.name],
cwd=directory, env=no_auth, capture_output=True, text=True)
if result.returncode:
raise ValueError("npm dependency acquisition failed: " + result.stderr.strip())
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit("npm prefetch refused: " + str(error))
print("npm packages cached without credentials or lifecycle execution")
PY_TRUSTED
unset TOKEN SUBMODULE_TOKEN
- name: Validate package and contracts without credentials
run: |
set -euo pipefail
source .ci-mirrors.env
npm test
npm run typecheck
+2
View File
@@ -1,3 +1,5 @@
*.test
*.out
dist/
node_modules/
*.tgz
+2 -2
View File
@@ -1,6 +1,6 @@
# Agent 工作说明(Agent-System-Core)
本仓开发遵循 Development-Guidelines(Dev 仓 `deps/development-guidelines`,gitlink 固定版本):
本仓开发遵循 Development-Guidelines(由 Dev 固定规范来源;新布局见 Dev components.lock.json,规则目录 .workspace/Development-Guidelines;历史布局仍读原 gitlink):
核心规则见其 `core/`,按任务触发阅读 `standards/` 专题;日常交付走 `workflows/delivery.md`。
## 任务启动门禁
@@ -15,7 +15,7 @@
## 仓内规则
- 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。
- 跨仓 gitlink 变更须经组合验证后在 Dev 仓推进;受保护分支经 PR 合入。
- 跨仓语言依赖/应用源码锁变更须经固定组合验证后在消费方推进;源码提交到所属仓;受保护分支经 PR 合入。
- 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。
## Gitea 与 Git 访问事实(2026-09-30 实测)
+3 -2
View File
@@ -8,6 +8,7 @@
在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行
`git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。
修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test`
验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。
修改 `ts/types.ts` 后显式运行 `npm run build`,核验 `npm run typecheck`
与包内容并提交生成出口。消费者通过固定 `@agent-system/contracts` 版本更新;Dev 的
`make test` 验证实际固定组合,不再复制契约到消费者源码。单独运行 Core 测试不能代替组合验证。
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
+1 -1
View File
@@ -1,3 +1,3 @@
module agent-system-core
module git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git
go 1.27
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@agent-system/contracts",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agent-system/contracts",
"version": "0.1.0",
"devDependencies": {
"typescript": "5.9.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@agent-system/contracts",
"version": "0.1.0",
"type": "module",
"description": "Agent-System public TypeScript wire contracts",
"files": ["package"],
"types": "./package/index.d.ts",
"exports": {
".": {
"types": "./package/index.d.ts",
"import": "./package/index.js"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"build": "node scripts/build-package.mjs",
"check:package": "node scripts/build-package.mjs --check",
"typecheck": "tsc -p tsconfig.package.json --noEmit",
"test": "npm run check:package && node --test scripts/test-package.mjs"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
+672
View File
@@ -0,0 +1,672 @@
export type ID = string;
export interface Project {
id: ID;
owner?: ID;
name: string;
createdAt: string;
updatedAt: string;
settings?: Record<string, unknown>;
}
export interface Conversation {
id: ID;
projectId: ID;
owner?: ID;
title: string;
profileId?: ID;
pinned?: boolean;
archived?: boolean;
allowCrossRead?: boolean;
crossPrivate?: boolean;
permissionMode?: 'default' | 'plan' | 'auto_edit' | '';
sessionNote?: string;
createdAt: string;
updatedAt: string;
}
export type Role = 'user' | 'assistant' | 'system';
export type PartType = 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref' | 'hitl' | 'plan' | 'error' | 'card' | 'check';
export interface Part {
type: PartType;
text?: string;
toolCall?: {
id: string;
name: string;
arguments?: string;
durationMs?: number;
};
toolResult?: {
callId: string;
status: 'ok' | 'error';
summary?: string;
truncated?: boolean;
};
artifact?: {
artifactId: ID;
name: string;
kind?: string;
};
hitl?: {
cardId: ID;
question: string;
resolved: boolean;
answer?: string;
};
plan?: PlanSnapshot;
card?: {
kind: string;
data: UINode[];
cardId?: ID;
};
error?: {
code: string;
message: string;
};
check?: {
passed: boolean;
reason?: string;
};
process?: boolean;
durationMs?: number;
}
export interface Message {
id: ID;
conversationId: ID;
runId?: ID;
role: Role;
parts: Part[];
createdAt: string;
excluded?: boolean;
variantGroup?: string;
variantIndex?: number;
}
export type RunKind = 'conversation' | 'subagent' | 'tool';
export type RunStatus = 'pending' | 'running' | 'waiting_hitl' | 'completed' | 'failed' | 'cancelled' | 'interrupted';
export type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'done';
export interface Usage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
durationMs: number;
toolCalls: number;
model?: string;
lastPromptTokens?: number;
lastCompletionTokens?: number;
cacheReadTokens?: number;
cacheWriteTokens?: number;
}
export interface Run {
id: ID;
parentRunId?: ID;
conversationId: ID;
projectId: ID;
owner?: ID;
kind: RunKind;
profileId?: ID;
status: RunStatus;
phase?: RunPhase;
agentName?: string;
createdAt: string;
endedAt?: string;
usage?: Usage;
error?: {
code: string;
message: string;
};
}
export interface Artifact {
id: ID;
projectId: ID;
owner?: ID;
name: string;
kind: string;
mime?: string;
size: number;
sourceRun?: ID;
createdAt: string;
updatedAt: string;
}
export interface PlanSnapshot {
planId: ID;
title: string;
steps: {
id: string;
text: string;
status: 'pending' | 'in_progress' | 'done' | 'skipped';
agentRun?: ID;
}[];
}
export interface TodoList {
conversationId: ID;
items: {
id: string;
text: string;
status: 'pending' | 'in_progress' | 'done';
}[];
}
export type StreamEventType = 'message.delta' | 'thinking.delta' | 'tool.call.delta' | 'tool.call' | 'tool.output' | 'tool.result' | 'run.phase' | 'run.check' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update' | 'agent.spawn' | 'agent.progress' | 'agent.result' | 'artifact.created' | 'artifact.updated' | 'run.completed' | 'run.failed' | 'error' | 'card.created' | 'card.updated' | 'card.status' | 'card.resolved' | 'surface.updated';
export interface StreamEvent<P = unknown> {
seq: number;
runId?: ID;
conversationId: ID;
ts: string;
v: number;
type: StreamEventType;
payload?: P;
}
export interface MessageDeltaPayload {
index: number;
text: string;
}
export interface ToolCallDeltaPayload {
index: number;
callId?: string;
name?: string;
delta: string;
}
export interface ToolOutputPayload {
callId: string;
stream: 'stdout' | 'stderr';
text: string;
}
export interface ToolResultPayload {
callId: string;
status: string;
summary?: string;
truncated?: boolean;
durationMs?: number;
}
export interface AgentSpawnPayload {
agentRun: ID;
name: string;
task: string;
}
export interface AgentProgressPayload {
agentRun: ID;
note?: string;
phase?: RunPhase;
}
export interface AgentResultPayload {
agentRun: ID;
status: RunStatus;
summary?: string;
}
export interface HITLResolvedPayload {
cardId: ID;
answer?: string;
kind: 'choice' | 'free' | 'timeout' | 'cancelled';
}
export interface RunCompletedPayload {
usage: Usage;
context?: ContextObservation;
}
interface ContextSection {
name: string;
estTokens: number;
}
export interface ContextObservation {
items: string[];
truncations?: string[];
estChars: number;
sections?: ContextSection[];
prefixHash?: string;
}
export interface RunCheckPayload {
passed: boolean;
reason?: string;
}
export interface RunFailedPayload {
code: string;
message: string;
}
export type CardKind = 'progress' | 'confirm' | 'compare' | 'review-list' | 'result' | 'error' | 'status';
export type CardStatus = 'active' | 'action_required' | 'done' | 'resolved' | 'failed' | 'dismissed';
export interface ActivityCard {
id: ID;
projectId: ID;
conversationId?: ID;
sourceRunId?: ID;
anchorMessageId?: ID;
kind: CardKind;
title: string;
status: CardStatus;
tone?: string;
body?: UINode[];
actions?: {
id: string;
label: string;
style?: 'primary' | 'default' | 'danger';
command?: string;
}[];
pinned?: boolean;
createdAt: string;
updatedAt: string;
}
export interface CardPayload {
card: ActivityCard;
}
export interface CardStatusPayload {
cardId: ID;
status: CardStatus;
tone?: string;
note?: string;
}
export interface CardResolvedPayload {
cardId: ID;
actionId?: string;
answer?: string;
}
export type HITLKind = 'choice' | 'input' | 'confirm' | 'form';
export interface HITLChoice {
id: string;
label: string;
description: string;
impact: string;
recommended?: boolean;
}
export interface HITLCard {
id: ID;
runId: ID;
conversationId: ID;
question: string;
kind: HITLKind;
choices?: HITLChoice[];
allowFreeInput: boolean;
freeInputHint?: string;
timeoutSec?: number | null;
form?: UINode[];
createdAt: string;
resolved: boolean;
anchorMessageId?: ID;
blocked?: boolean;
blockReason?: string;
detail?: string;
}
export interface HITLResponse {
cardId: ID;
choiceId?: string;
freeText?: string;
formData?: unknown;
}
export type UINodeType = 'section' | 'list' | 'kv' | 'form' | 'markdown' | 'doc-ref' | 'actions' | 'chart-lite' | 'avatar' | 'progress' | 'compare' | 'tree' | 'table' | 'steps' | 'tabs' | 'timeline';
export interface UINode {
type: UINodeType;
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
id?: string;
source?: string[];
title?: string;
items?: string[];
kv?: [string, string][];
fields?: UIField[];
markdown?: string;
docId?: ID;
actions?: {
id: string;
label: string;
style?: 'primary' | 'default' | 'danger';
}[];
chart?: {
kind: 'bar' | 'line';
labels: string[];
series: number[];
};
children?: UINode[];
avatar?: {
name?: string;
sub?: string;
tone?: string;
stack?: {
name?: string;
sub?: string;
tone?: string;
}[];
};
progress?: {
label?: string;
value: number;
tone?: string;
};
compare?: {
left: {
title?: string;
tone?: string;
nodes?: UINode[];
};
right: {
title?: string;
tone?: string;
nodes?: UINode[];
};
};
tree?: {
nodes: {
id: string;
label: string;
tone?: string;
badge?: string;
count?: number;
selected?: boolean;
children?: unknown[];
}[];
};
table?: {
columns: {
key: string;
label: string;
}[];
rows: {
id?: string;
cells: [string, string][];
actions?: UINode['actions'];
}[];
};
steps?: {
current: number;
steps: {
label: string;
status?: 'pending' | 'active' | 'done' | 'failed';
tone?: string;
}[];
};
tabs?: {
active?: string;
tabs: {
id: string;
label: string;
nodes?: UINode[];
}[];
};
timeline?: {
items: {
time?: string;
title: string;
tone?: string;
markdown?: string;
ref?: string;
}[];
};
}
export interface UIField {
key: string;
label: string;
type: 'text' | 'textarea' | 'number' | 'boolean' | 'select';
options?: string[];
required?: boolean;
placeholder?: string;
help?: string;
default?: string;
}
export interface PromptTrigger {
kind: 'intent' | 'command' | 'attachment' | 'manual';
keywords?: string[];
command?: string;
}
export interface PromptModule {
id: string;
layer: 'base' | 'env' | 'task' | 'agent' | 'tool' | 'aux' | 'style';
version: number;
title: string;
description?: string;
priority?: number;
triggers?: PromptTrigger[];
requiresAttachments?: boolean;
body: string;
outputContract?: string;
source?: 'builtin' | 'plugin' | 'user';
owner?: string;
basedOnVersion?: number;
drifted?: boolean;
disabled?: boolean;
/** 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。 */
tools?: string[];
model?: string;
maxTurns?: number;
}
export interface PromptBlock {
moduleId: string;
version: number;
layer: string;
body: string;
}
export interface PromptHitRecord {
moduleId: string;
layer: string;
reason: string;
injected: boolean;
}
export interface AgentProfile {
id: string;
owner?: string;
name: string;
description?: string;
builtin?: boolean;
source?: string;
systemPrompt: string;
/** 提示词库模块引用(有序;空 = 默认 base+env 全集)。 */
promptModules?: string[];
model: {
chat?: string;
lightweight?: string;
embedding?: string;
rerank?: string;
};
tools: {
allow: string[];
deny?: string[];
confirm?: string[];
};
context: {
maxHistoryMessages: number;
summarizeBeyond?: boolean;
memoryBudgetPct?: number;
knowledgeBudgetPct?: number;
maxToolResultChars?: number;
};
hitl: {
askOnAmbiguity: boolean;
timeoutSec?: number | null;
};
budget: {
maxTokens?: number | null;
maxToolCalls?: number | null;
maxTurns?: number | null;
};
}
export interface SettingDesc {
key: string;
group: string;
label: string;
description?: string;
type: 'string' | 'number' | 'boolean' | 'enum' | 'secret';
default?: unknown;
options?: string[];
effect: 'hot' | 'new-run' | 'restart';
secret?: boolean;
/** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */
scope?: 'system';
}
export interface PluginManifest {
id: string;
name: string;
version: string;
apiVersion: string;
requires?: {
network?: string[];
};
provides: {
tools?: string[];
workflows?: string[];
intents?: string[];
prompts?: string[];
panels?: string[];
cards?: string[];
settings?: string[];
commands?: string[];
workspaces?: string[];
skills?: string[];
mcpServers?: string[];
knowledge?: string[];
parsers?: string[];
agents?: string[];
channels?: string[];
pages?: string[];
};
scope?: {
tenants?: string[];
projects?: string[];
};
backend?: {
mode?: 'inproc' | 'sidecar';
command?: string;
args?: string[];
schemaVersion?: number;
};
}
export interface PluginPanel {
id: string;
title: string;
icon?: string;
body: UINode[];
refreshTool?: string;
}
/** 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。 */
export interface WorkflowDef {
id: string;
title: string;
description?: string;
intentId?: string;
/** 整条工作流的 critic 重试上限。 */
maxRetries?: number;
steps: WorkflowStep[];
}
export interface StepCritic {
criteria: string;
/** 本步重试上限;缺省 1 次。 */
maxRetries?: number;
}
export interface WorkflowStep {
id: string;
title: string;
kind: 'prompt' | 'tool' | 'hitl' | 'artifact' | 'card' | 'script';
prompt?: string;
/** prompt 步骤的子代理 profile,缺省 general。 */
agent?: string;
tool?: string;
args?: unknown;
hitl?: {
question: string;
kind: HITLKind;
choices?: HITLChoice[];
allowFreeInput: boolean;
freeInputHint?: string;
};
artifact?: string;
card?: {
kind: CardKind;
title: string;
tone?: string;
body?: UINode[];
actions?: {
id: string;
label: string;
style?: 'primary' | 'default' | 'danger';
command?: string;
}[];
};
/** script 步骤在插件目录内的相对路径。 */
script?: string;
critic?: StepCritic;
}
export interface PluginWorkspace {
id: string;
title: string;
icon?: string;
shortcut?: string;
badgeTool?: string;
body: UINode[];
refreshTool?: string;
}
export interface PluginPageDecl {
id: string;
title: string;
module: string;
}
export interface ParsedDocument {
meta: {
filename: string;
mime?: string;
parser: string;
confidence?: number;
crossCheck?: {
with: string;
diffBlocks: number;
verdict: 'agree' | 'differ';
};
parsedAt: string;
};
blocks: {
type: 'heading' | 'para' | 'list' | 'table' | 'code' | 'image-ref';
level?: number;
text?: string;
items?: string[];
rows?: string[][];
ref?: string;
confidence?: number;
}[];
warnings?: string[];
}
export interface RetrievalHit {
doc: {
id: ID;
owner?: ID;
projectId?: ID;
source: string;
title: string;
ref?: string;
chunk: string;
createdAt: string;
};
score: number;
kind: 'fulltext' | 'vector' | 'plugin';
}
export interface UploadInfo {
artifactId: ID;
filename: string;
mime?: string;
size: number;
parsed?: ParsedDocument;
}
export interface SurfaceCompat {
schemaVersion: number;
catalogVersion: string;
pluginVersion?: string;
}
export interface SurfaceIdentity {
surfaceId: ID;
owner: string;
projectId?: ID;
conversationId?: ID;
sourceRunId?: ID;
pluginId: string;
}
export interface SurfaceSpec {
title: string;
nodes: UINode[];
}
export interface SurfaceSnapshot {
identity: SurfaceIdentity;
compat: SurfaceCompat;
revision: number;
baseRevision?: number;
contextVersion?: string;
activationEpoch: number;
spec: SurfaceSpec;
semanticText?: string;
}
export interface SurfaceMeta {
surfaceId: ID;
title: string;
pluginId: string;
revision: number;
conversationId?: ID;
updatedAt: string;
}
export {};
+4
View File
@@ -0,0 +1,4 @@
// Agent-System-Core 契约的 TypeScript 镜像。
// 线格式契约以 Go 类型(camelCase JSON 标签)与 testdata/golden/*.json 黄金样本为准;
// 本文件手工镜像同一契约,前端按未知 type/字段静默忽略(前向兼容原则)。
export {};
+24
View File
@@ -0,0 +1,24 @@
// Explicit generation only: authenticated Git acquisition must never run package scripts.
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = dirname(dirname(fileURLToPath(import.meta.url)))
const check = process.argv.includes('--check')
const output = check ? mkdtempSync(join(tmpdir(), 'agent-contracts-build-')) : join(root, 'package')
try {
mkdirSync(output, { recursive: true })
execFileSync(process.execPath, [join(root, 'node_modules/typescript/bin/tsc'), '-p', join(root, 'tsconfig.package.json'), '--outDir', output], { cwd: root, stdio: 'inherit' })
for (const extension of ['js', 'd.ts']) renameSync(join(output, `types.${extension}`), join(output, `index.${extension}`))
if (check) {
for (const file of ['index.js', 'index.d.ts']) {
if (!readFileSync(join(root, 'package', file)).equals(readFileSync(join(output, file)))) {
throw new Error(`package/${file} is stale; run npm run build before committing the provider`)
}
}
}
} finally {
if (check) rmSync(output, { recursive: true, force: true })
}
+46
View File
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { test } from 'node:test'
const root = dirname(dirname(fileURLToPath(import.meta.url)))
test('packed contracts compile in a consumer without the Core checkout', () => {
const consumer = mkdtempSync(join(tmpdir(), 'agent-contracts-consumer-'))
try {
const pack = spawnSync('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', consumer], {
cwd: root, encoding: 'utf8',
})
assert.equal(pack.status, 0, `Core must expose an independently packable contracts package:\n${pack.stderr}`)
const [artifact] = JSON.parse(pack.stdout)
assert.equal(artifact.name, '@agent-system/contracts')
assert.ok(artifact.files.some(file => file.path === 'package/index.d.ts'), 'declarations must be packed')
assert.ok(artifact.files.some(file => file.path === 'package/index.js'), 'ESM entry must be packed')
assert.ok(!artifact.files.some(file => file.path.endsWith('.go')), 'Go implementation is outside the npm package')
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' }))
writeFileSync(join(consumer, 'consumer.ts'), `
import type { ActivityCard, Message, SurfaceSnapshot } from '@agent-system/contracts'
const card: ActivityCard = { id: 'card', projectId: 'project', conversationId: 'conversation', kind: 'progress', status: 'active', title: 'Working', createdAt: '2026-10-13T00:00:00Z', updatedAt: '2026-10-13T00:00:00Z' }
const message: Message = { id: 'message', conversationId: 'conversation', role: 'assistant', parts: [{ type: 'text', text: card.title }], createdAt: card.createdAt }
type Surface = SurfaceSnapshot
// @ts-expect-error unknown message roles are not part of the wire contract
const invalidRole: Message['role'] = 'operator'
export { message, invalidRole }; export type { Surface }
`)
const tsc = [join(root, 'node_modules/typescript/bin/tsc'), '--noEmit', '--strict', '--skipLibCheck', '--module', 'NodeNext', '--moduleResolution', 'NodeNext', 'consumer.ts']
const missing = spawnSync(process.execPath, tsc, { cwd: consumer, encoding: 'utf8' })
assert.notEqual(missing.status, 0, 'the consumer must not resolve Core types before package installation')
assert.match(missing.stdout, /TS2307.*@agent-system\/contracts/)
execFileSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--offline', '--package-lock=false', join(consumer, artifact.filename)], { cwd: consumer, stdio: 'pipe' })
execFileSync(process.execPath, tsc, { cwd: consumer, stdio: 'inherit' })
execFileSync(process.execPath, ['--input-type=module', '-e', "import * as contracts from '@agent-system/contracts'; if (Object.keys(contracts).length) throw Error('contracts must remain type-only')"], { cwd: consumer, stdio: 'pipe' })
const declaration = readFileSync(join(consumer, 'node_modules/@agent-system/contracts/package/index.d.ts'), 'utf8')
assert.match(declaration, /export interface SurfaceSnapshot/)
} finally {
rmSync(consumer, { recursive: true, force: true })
}
})
+16
View File
@@ -0,0 +1,16 @@
# TypeScript contracts package
`ts/types.ts` is the sole TypeScript contract source. `npm run build` emits the
tracked `package/index.d.ts` and type-only ESM entry for `@agent-system/contracts`.
Consumers import types from the package instead of copying this source.
Run `npm ci --ignore-scripts`, remove acquisition credentials from the execution
environment, then run `npm run build && npm test`. `npm test` detects stale
generated output and installs an `npm pack --ignore-scripts` artifact into an
isolated consumer that has no Core checkout.
The generated package is committed with its source so fixed Git dependencies work
with lifecycle scripts disabled. The package has no `prepare`, `install`, or
`prepack` hook. Pin a reachable provider commit in each consumer's manifest and
lockfile. This package version is metadata; no Registry upload or release tag is
created by these commands.
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"rootDir": "ts",
"outDir": "package",
"skipLibCheck": true
},
"files": ["ts/types.ts"]
}