Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,21 @@ ECC takes supply-chain and agent safety seriously.
- **Built-in guardrails.** GateGuard gates destructive shell commands (including `rm`, force/path `git checkout`, and destructive `find -exec`) before they run; the supply-chain IOC scanner runs in CI; and [AgentShield](#agentshield--security-auditor) audits your own agent, hook, MCP, permission, and secret surfaces (`/security-scan`).
- **Deep dive.** See the [Security Guide](./the-security-guide.md).

### Optional CLI usage telemetry

CLI telemetry is **off by default** and never runs during package installation.
Only `ecc telemetry enable` persists consent; `ECC_TELEMETRY=0` is a hard
override. Use `ecc telemetry preview` to inspect the complete local event and
`ecc telemetry schema` to print its public allowlist. Events never contain
arguments, prompts, paths, usernames, repository names, credentials, RFQs, or
supplier/demand data; sponsor-specific commands are excluded from collection.
Delivery is inert unless an HTTPS endpoint is separately configured, and
deletion remains bound to every original receiving endpoint.

See the
[optional telemetry architecture and privacy contract](docs/architecture/optional-cli-usage-telemetry.md)
before configuring collection.

---

## Sponsors
Expand Down
130 changes: 130 additions & 0 deletions docs/architecture/optional-cli-usage-telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Optional ECC CLI Usage Telemetry

ECC's default remains local-first: CLI usage telemetry is off unless a user
explicitly runs `ecc telemetry enable`. Installing or updating the npm package
does not enable telemetry, create an identifier, or make a telemetry request.
The package has no `preinstall`, `install`, or `postinstall` telemetry hook.

This client is an endpoint-gated candidate. The public package does not contain
a default collection endpoint. An event can leave the machine only when both
of these conditions are true:

1. the user has run `ecc telemetry enable`; and
2. `ECC_TELEMETRY_ENDPOINT` names a valid HTTPS endpoint.

Do not add a default endpoint until an owner has published the privacy notice,
reviewed the lawful basis and processor contracts, and verified the server-side
retention, deletion, and source-IP controls below.

## User Controls

```bash
ecc telemetry status
ecc telemetry preview --command consult --result success --latency-ms 250
ecc telemetry schema
ecc telemetry enable
ecc telemetry disable
ecc telemetry delete
ecc telemetry delete --local-only
ECC_TELEMETRY=0 ecc doctor
```

- `status` shows persisted consent, effective collection, endpoint readiness,
identifier presence, rotation, and retention without printing a local path
or identifier.
- `preview` builds an ephemeral, local-only event without persisting consent or
sending a request.
- `schema` prints the event allowlist in
`schemas/ecc-cli-telemetry-event.schema.json`.
- `enable` is the only way to persist consent. Setting an environment variable
cannot opt a user in.
- `disable` stops future collection and retains only the rotating identities
and bound endpoints needed for a later remote deletion request.
- Before an event is sent, its rotating identifier is durably bound to the
normalized endpoint that will receive it. `delete` sends the deletion
contract to every original bound endpoint and clears only endpoint/identifier
bindings whose deletion succeeded. Partial failures remain locally retryable.
- `delete --local-only` clears local state without making a request.
- `ECC_TELEMETRY=0` is a hard override, including for remote deletion. Remove
the override temporarily or use `--local-only` if deletion is required.

## Complete Event Allowlist

The event has `additionalProperties: false` and contains only:

| Field | Value |
| --- | --- |
| `schemaVersion` | Fixed schema identifier |
| `anonymousId` | Random UUID rotated every 30 days |
| `packageName` / `packageVersion` | Published ECC package and version |
| `commandId` | Allowlisted sponsor-neutral top-level command or `other` |
| `result` | `success` or `failure` |
| `latencyBucket` | One of four coarse duration ranges |
| `os` | `macos`, `linux`, `windows`, or `other` |
| `arch` | `arm64`, `x64`, or `other` |

There is no event timestamp; the ingestion service can use receipt time. ECC
does not capture or send command arguments, prompts, file paths, usernames,
repository names, hostnames, IP addresses as an event field, credentials,
cookies, RFQs, supplier details, demand details, prices, or quantities.

The sponsor-specific `ito` command is deliberately excluded from collection;
telemetry cannot measure sponsor or RFQ usage.

HTTP necessarily exposes a source address to the receiving network edge. The
ingestion service must disable source-IP storage in access logs and analytics,
must not enrich events with geolocation or organization identity, and must not
set cookies or join this identifier to account, marketing, RFQ, or billing data.

## Endpoint Contract

`ECC_TELEMETRY_ENDPOINT` must use HTTPS and cannot contain credentials, a query
string, or a fragment. The client binds the normalized endpoint to the
identifier before attempting delivery, so changing the environment later
cannot redirect a deletion request away from the original recipient. The
client sends:

- `POST` with `ecc.cli-telemetry.v1`; or
- `DELETE` with `ecc.cli-telemetry-deletion.v1`.

Delivery is best-effort, has a 750 ms timeout, and never changes the wrapped
command's result. Failed events are dropped rather than queued on disk.
Cross-process state locking serializes rotation, delivery, and deletion so an
emitted identifier remains covered by deletion and a deletion cannot race a
later event. The server must:

- enforce the public schemas and reject additional properties;
- retain raw pseudonymous events for no more than 30 days;
- delete matching raw events when it accepts a deletion request;
- strip the rotating identifier before retaining longer-lived aggregate
counts;
- suppress small cohorts before publishing breakdowns; and
- publish current controller, processor, contact, purpose, retention, and
deletion information.

The client displaying a 30-day ceiling does not prove server compliance. A
default endpoint must remain absent until these controls are independently
verified.

## What The Analytics Can And Cannot Answer

npm exposes package-level download counts, which can help measure aggregate
adoption, but those counts do not reveal which ECC commands or workflows were
used. Explicitly opted-in CLI events can estimate aggregate command usage,
failure rates, broad performance buckets, and platform compatibility needs.
They are a self-selected sample, not a user census, and must not be used to
identify or target individual people, companies, repositories, suppliers, or
demand.

Relevant primary sources:

- [npm package search and aggregate download-count sorting](https://docs.npmjs.com/searching-for-and-choosing-packages-to-download/)
- [npm lifecycle-script behavior](https://docs.npmjs.com/cli/using-npm/scripts/)
- [GDPR Articles 5, 6, 13, and 25](https://eur-lex.europa.eu/eli/reg/2016/679/oj)
- [California Privacy Protection Agency data-minimization advisory](https://cppa.ca.gov/pdf/enfadvisory202401.pdf)

This design is a technical minimization baseline, not legal advice. Consent
does not by itself settle jurisdiction, controller obligations, employee-use
issues, international transfers, or whether a pseudonymous identifier is
personal information. Counsel should review the final endpoint, notice, and
retention implementation before any public collection endpoint is configured.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"docs/vi-VN/",
"docs/zh-CN/",
"docs/zh-TW/",
"docs/architecture/optional-cli-usage-telemetry.md",
"hooks/",
"install.ps1",
"install.sh",
Expand Down Expand Up @@ -124,6 +125,7 @@
"scripts/setup-package-manager.js",
"scripts/skill-create-output.js",
"scripts/status.js",
"scripts/telemetry.js",
"scripts/sync-ecc-to-codex.sh",
"scripts/work-items.js",
"scripts/uninstall.js",
Expand Down
28 changes: 28 additions & 0 deletions schemas/ecc-cli-telemetry-deletion.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/affaan-m/ECC/schemas/ecc-cli-telemetry-deletion.schema.json",
"title": "ECC CLI telemetry deletion request",
"description": "The complete payload for an explicit telemetry deletion request.",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"anonymousIds"
],
"properties": {
"schemaVersion": {
"const": "ecc.cli-telemetry-deletion.v1"
},
"anonymousIds": {
"type": "array",
"description": "Identifiers bound to the same original collection endpoint.",
"minItems": 1,
"maxItems": 64,
"uniqueItems": true,
"items": {
"type": "string",
"format": "uuid"
}
}
}
}
87 changes: 87 additions & 0 deletions schemas/ecc-cli-telemetry-event.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/affaan-m/ECC/schemas/ecc-cli-telemetry-event.schema.json",
"title": "ECC privacy-safe CLI usage event",
"description": "The complete allowlist for an explicitly opted-in ECC CLI usage event. No command arguments or user/workspace data are permitted.",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"anonymousId",
"packageName",
"packageVersion",
"commandId",
"result",
"latencyBucket",
"os",
"arch"
],
"properties": {
"schemaVersion": {
"const": "ecc.cli-telemetry.v1"
},
"anonymousId": {
"type": "string",
"format": "uuid",
"description": "Random pseudonymous identifier rotated every 30 days."
},
"packageName": {
"const": "ecc-universal"
},
"packageVersion": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
},
"commandId": {
"enum": [
"install",
"plan",
"catalog",
"consult",
"control-pane",
"list-installed",
"doctor",
"repair",
"auto-update",
"status",
"platform-audit",
"security-ioc-scan",
"sessions",
"work-items",
"session-inspect",
"loop-status",
"uninstall",
"other"
]
},
"result": {
"enum": [
"success",
"failure"
]
},
"latencyBucket": {
"enum": [
"under_100ms",
"100ms_to_1s",
"1s_to_10s",
"10s_or_more"
]
},
"os": {
"enum": [
"macos",
"linux",
"windows",
"other"
]
},
"arch": {
"enum": [
"arm64",
"x64",
"other"
]
}
}
}
56 changes: 52 additions & 4 deletions scripts/ecc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require('path');
const { listAvailableLanguages } = require('./lib/install-executor');
const { getComputeSponsorCopy } = require('./lib/compute-sponsor');
const { createSafeItoInvocationEnvironment } = require('./lib/ito-environment');
const { recordCommandUsage } = require('./lib/cli-telemetry');

const COMMANDS = {
install: {
Expand All @@ -31,6 +32,10 @@ const COMMANDS = {
script: 'ito.js',
description: 'Invoke the separately installed canonical Itô compute CLI',
},
telemetry: {
script: 'telemetry.js',
description: 'Control optional privacy-safe CLI usage telemetry (default off)',
},
'install-plan': {
script: 'install-plan.js',
description: 'Alias for plan',
Expand Down Expand Up @@ -92,6 +97,7 @@ const PRIMARY_COMMANDS = [
'consult',
'control-pane',
'ito',
'telemetry',
'list-installed',
'doctor',
'repair',
Expand Down Expand Up @@ -142,6 +148,10 @@ Examples:
ecc ito find --gpu h200 --count 8 --nodes 1 --gpus-per-node 8 --days 30 --storage-tb 1 --start-window 2099-08-15 --max-rate 3.00 --form-factor bare_metal --contract-type reservation --fabric infiniband --region us-east-1
ecc ito status --json
ecc ito evals --cluster clu_prod_example --live-sixtytwo --nodes gpu-01,gpu-02 --config-dir /absolute/path/to/qualification-config
ecc telemetry status
ecc telemetry preview --command consult --result success --latency-ms 250
ecc telemetry enable
ECC_TELEMETRY=0 ecc doctor
ecc list-installed --json
ecc doctor --target cursor
ecc repair --dry-run
Expand Down Expand Up @@ -267,7 +277,17 @@ function runCommand(commandName, args) {
return 1;
}

function main() {
async function recordCommandUsageSafely(input, recorder = recordCommandUsage) {
try {
return await recorder(input);
} catch {
return { sent: false, reason: 'telemetry_unavailable' };
}
}

async function main() {
let trackedCommand = null;
const startedAt = Date.now();
try {
const resolution = resolveCommand(process.argv);

Expand All @@ -288,11 +308,39 @@ function main() {
return;
}

process.exitCode = runCommand(resolution.command, resolution.args);
trackedCommand = resolution.command;
const exitCode = runCommand(resolution.command, resolution.args);
if (trackedCommand !== 'telemetry' && trackedCommand !== 'ito') {
await recordCommandUsageSafely({
commandId: trackedCommand,
exitCode,
elapsedMs: Date.now() - startedAt,
});
}
process.exitCode = exitCode;
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
if (trackedCommand && trackedCommand !== 'telemetry' && trackedCommand !== 'ito') {
await recordCommandUsageSafely({
commandId: trackedCommand,
exitCode: 1,
elapsedMs: Date.now() - startedAt,
});
}
process.exitCode = 1;
}
}

main();
if (require.main === module) {
main().catch(error => {
console.error(`Error: ${error.message}`);
process.exitCode = 1;
});
}

module.exports = Object.freeze({
main,
recordCommandUsageSafely,
resolveCommand,
runCommand,
});
Loading